package fleetctl

import (
	"context"
	"crypto/sha256"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/fleetdm/fleet/v4/cmd/fleetctl/fleetctl/testing_utils"
	"github.com/fleetdm/fleet/v4/pkg/file"
	"github.com/fleetdm/fleet/v4/pkg/optjson"
	"github.com/fleetdm/fleet/v4/server/config"
	"github.com/fleetdm/fleet/v4/server/datastore/mysql"
	"github.com/fleetdm/fleet/v4/server/fleet"
	apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
	"github.com/fleetdm/fleet/v4/server/mdm/apple/vpp"
	"github.com/fleetdm/fleet/v4/server/mdm/nanodep/tokenpki"
	mdmtesting "github.com/fleetdm/fleet/v4/server/mdm/testing_utils"
	"github.com/fleetdm/fleet/v4/server/mock"
	digicert_mock "github.com/fleetdm/fleet/v4/server/mock/digicert"
	mdmmock "github.com/fleetdm/fleet/v4/server/mock/mdm"
	scep_mock "github.com/fleetdm/fleet/v4/server/mock/scep"
	"github.com/fleetdm/fleet/v4/server/ptr"
	"github.com/fleetdm/fleet/v4/server/service"
	"github.com/google/uuid"
	"github.com/jmoiron/sqlx"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

const (
	teamName       = "Team Test"
	fleetServerURL = "https://fleet.example.com"
	orgName        = "GitOps Test"
)

// setupDefaultTeamConfigMocks sets up the mock functions for DefaultTeamConfig operations
// needed for No Team webhook settings in premium license tests
func setupDefaultTeamConfigMocks(ds interface{}) {
	switch d := ds.(type) {
	case *mock.DataStore:
		d.DefaultTeamConfigFunc = func(ctx context.Context) (*fleet.TeamConfig, error) {
			return &fleet.TeamConfig{}, nil
		}
		d.SaveDefaultTeamConfigFunc = func(ctx context.Context, config *fleet.TeamConfig) error {
			return nil
		}
		d.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
			return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
		}
	case *mock.Store:
		// mock.Store embeds DataStore, so we can set the functions on the embedded struct
		d.DefaultTeamConfigFunc = func(ctx context.Context) (*fleet.TeamConfig, error) {
			return &fleet.TeamConfig{}, nil
		}
		d.SaveDefaultTeamConfigFunc = func(ctx context.Context, config *fleet.TeamConfig) error {
			return nil
		}
		d.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
			return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
		}
	}
}

func TestGitOpsFilenameValidation(t *testing.T) {
	filename := strings.Repeat("a", filenameMaxLength+1)
	_, err := RunAppNoChecks([]string{"gitops", "-f", filename})
	assert.ErrorContains(t, err, "file name must be less than")
}

func TestGitOpsBasicGlobalFree(t *testing.T) {
	// Cannot run t.Parallel() because it sets environment variables

	_, ds := testing_utils.RunServerWithMockedDS(t)

	ds.BatchSetMDMProfilesFunc = func(
		ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
		macDecls []*fleet.MDMAppleDeclaration, androidProfiles []*fleet.MDMAndroidConfigProfile, vars []fleet.MDMProfileIdentifierFleetVariables,
	) (updates fleet.MDMProfilesUpdates, err error) {
		return fleet.MDMProfilesUpdates{}, nil
	}
	ds.BulkSetPendingMDMHostProfilesFunc = func(
		ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
	) (updates fleet.MDMProfilesUpdates, err error) {
		return fleet.MDMProfilesUpdates{}, nil
	}
	ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) ([]fleet.ScriptResponse, error) {
		return []fleet.ScriptResponse{}, nil
	}
	ds.NewActivityFunc = func(
		ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
	) error {
		return nil
	}
	ds.ListGlobalPoliciesFunc = func(ctx context.Context, opts fleet.ListOptions) ([]*fleet.Policy, error) { return nil, nil }
	ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
		return nil, 0, nil, nil
	}
	ds.ListTeamPoliciesFunc = func(
		ctx context.Context, teamID uint, opts fleet.ListOptions, iopts fleet.ListOptions,
	) (teamPolicies []*fleet.Policy, inheritedPolicies []*fleet.Policy, err error) {
		return nil, nil, nil
	}

	// Mock appConfig
	savedAppConfig := &fleet.AppConfig{}
	ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
		return &fleet.AppConfig{}, nil
	}
	ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
		savedAppConfig = config
		return nil
	}
	ds.GetLabelSpecsFunc = func(ctx context.Context) ([]*fleet.LabelSpec, error) {
		return nil, nil
	}

	var enrolledSecrets []*fleet.EnrollSecret
	ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
		enrolledSecrets = secrets
		return nil
	}

	ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
		return nil
	}

	ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
		return []*fleet.VPPTokenDB{}, nil
	}

	ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
		return []*fleet.ABMToken{}, nil
	}

	// Mock DefaultTeamConfig functions for No Team webhook settings
	setupDefaultTeamConfigMocks(ds)

	tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
	require.NoError(t, err)

	const (
		fleetServerURL = "https://fleet.example.com"
		orgName        = "GitOps Test"
	)
	t.Setenv("FLEET_SERVER_URL", fleetServerURL)

	_, err = tmpFile.WriteString(
		`
controls:
queries:
policies:
agent_options:
org_settings:
  server_settings:
    server_url: $FLEET_SERVER_URL
  org_info:
    contact_url: https://example.com/contact
    org_logo_url: ""
    org_logo_url_light_background: ""
    org_name: ${ORG_NAME}
  secrets:
`,
	)
	require.NoError(t, err)

	// No file
	var errWriter strings.Builder
	_, err = RunAppNoChecks([]string{"gitops", tmpFile.Name()})
	require.Error(t, err)
	assert.Equal(t, `Required flag "f" not set`, err.Error())

	// Blank file
	errWriter.Reset()
	_, err = RunAppNoChecks([]string{"gitops", "-f", ""})
	require.Error(t, err)
	assert.Contains(t, err.Error(), "file name cannot be empty")

	// Bad file
	errWriter.Reset()
	_, err = RunAppNoChecks([]string{"gitops", "-f", "fileDoesNotExist.yml"})
	require.Error(t, err)
	assert.Contains(t, err.Error(), "no such file or directory")

	// Empty file
	errWriter.Reset()
	badFile, err := os.CreateTemp(t.TempDir(), "*.yml")
	require.NoError(t, err)
	_, err = RunAppNoChecks([]string{"gitops", "-f", badFile.Name()})
	require.Error(t, err)
	assert.Contains(t, err.Error(), "errors occurred")

	// DoGitOps error
	t.Setenv("ORG_NAME", "")
	_, err = RunAppNoChecks([]string{"gitops", "-f", tmpFile.Name()})
	require.Error(t, err)
	assert.Contains(t, err.Error(), "organization name must be present")

	// Missing controls.
	tmpFile2, err := os.CreateTemp(t.TempDir(), "*.yml")
	require.NoError(t, err)
	_, err = tmpFile2.WriteString(
		`
queries:
policies:
agent_options:
labels:
org_settings:
  server_settings:
    server_url: https://example.com
  org_info:
    contact_url: https://example.com/contact
    org_name: Foobar
  secrets:
`,
	)
	require.NoError(t, err)
	_, err = RunAppNoChecks([]string{"gitops", "-f", tmpFile2.Name()})
	require.Error(t, err)
	assert.Equal(t, `'controls' must be set on global config`, err.Error())

	// Dry run
	t.Setenv("ORG_NAME", orgName)
	_ = RunAppForTest(t, []string{"gitops", "-f", tmpFile.Name(), "--dry-run"})
	assert.Equal(t, fleet.AppConfig{}, *savedAppConfig, "AppConfig should be empty")

	// Real run
	_ = RunAppForTest(t, []string{"gitops", "-f", tmpFile.Name()})
	assert.Equal(t, orgName, savedAppConfig.OrgInfo.OrgName)
	assert.Equal(t, fleetServerURL, savedAppConfig.ServerSettings.ServerURL)
	assert.Empty(t, enrolledSecrets)
}

func TestGitOpsBasicGlobalPremium(t *testing.T) {
	// Cannot run t.Parallel() because it sets environment variables

	license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
	scepConfig := &scep_mock.SCEPConfigService{}
	scepConfig.ValidateSCEPURLFunc = func(_ context.Context, _ string) error { return nil }
	scepConfig.ValidateNDESSCEPAdminURLFunc = func(_ context.Context, _ fleet.NDESSCEPProxyCA) error { return nil }
	digiCertService := &digicert_mock.Service{}
	digiCertService.VerifyProfileIDFunc = func(_ context.Context, _ fleet.DigiCertCA) error { return nil }
	_, ds := testing_utils.RunServerWithMockedDS(
		t, &service.TestServerOpts{
			License:           license,
			KeyValueStore:     testing_utils.NewMemKeyValueStore(),
			EnableSCEPProxy:   true,
			SCEPConfigService: scepConfig,
			DigiCertService:   digiCertService,
		},
	)

	ds.BatchSetMDMProfilesFunc = func(
		ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
		macDecls []*fleet.MDMAppleDeclaration, androidProfiles []*fleet.MDMAndroidConfigProfile, vars []fleet.MDMProfileIdentifierFleetVariables,
	) (updates fleet.MDMProfilesUpdates, err error) {
		return fleet.MDMProfilesUpdates{}, nil
	}
	ds.BulkSetPendingMDMHostProfilesFunc = func(
		ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
	) (updates fleet.MDMProfilesUpdates, err error) {
		return fleet.MDMProfilesUpdates{}, nil
	}
	ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) ([]fleet.ScriptResponse, error) {
		return []fleet.ScriptResponse{}, nil
	}
	ds.NewActivityFunc = func(
		ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
	) error {
		return nil
	}
	ds.ListGlobalPoliciesFunc = func(ctx context.Context, opts fleet.ListOptions) ([]*fleet.Policy, error) { return nil, nil }
	ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
		return nil, 0, nil, nil
	}

	// Mock DefaultTeamConfig functions for No Team webhook settings
	setupDefaultTeamConfigMocks(ds)

	// Mock appConfig
	savedAppConfig := &fleet.AppConfig{}
	ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
		return &fleet.AppConfig{
			// Set a GitOps UI mode to verify that applying GitOps config won't overwrite it.
			UIGitOpsMode: fleet.UIGitOpsModeConfig{
				GitopsModeEnabled: true,
				RepositoryURL:     "https://didsomeonesaygitops.biz",
			},
		}, nil
	}
	ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
		savedAppConfig = config
		return nil
	}
	ds.GetLabelSpecsFunc = func(ctx context.Context) ([]*fleet.LabelSpec, error) {
		return nil, nil
	}

	var enrolledSecrets []*fleet.EnrollSecret
	ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
		enrolledSecrets = secrets
		return nil
	}
	ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
		return map[string]uint{labels[0]: 1}, nil
	}
	ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
		return &fleet.MDMAppleDeclaration{}, nil
	}
	ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
		return &fleet.Job{}, nil
	}
	ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
		return nil
	}
	ds.BatchSetInHouseAppsInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
		return nil
	}
	ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
		return nil, nil
	}

	ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
		return nil
	}

	ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
		return []*fleet.VPPTokenDB{}, nil
	}

	ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
		return []*fleet.ABMToken{}, nil
	}

	ds.DeleteSetupExperienceScriptFunc = func(ctx context.Context, teamID *uint) error {
		return nil
	}
	ds.ListTeamPoliciesFunc = func(
		ctx context.Context, teamID uint, opts fleet.ListOptions, iopts fleet.ListOptions,
	) (teamPolicies []*fleet.Policy, inheritedPolicies []*fleet.Policy, err error) {
		return nil, nil, nil
	}
	ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
		return nil
	}

	// we'll use to mock datastore persistence
	var storedCAs fleet.GroupedCertificateAuthorities
	muStoredCAs := sync.Mutex{}

	ds.BatchApplyCertificateAuthoritiesFunc = func(ctx context.Context, ops fleet.CertificateAuthoritiesBatchOperations) error {
		muStoredCAs.Lock()
		defer muStoredCAs.Unlock()
		if len(ops.Delete) > 0 {
			storedCAs = fleet.GroupedCertificateAuthorities{}
		}
		upserts := make([]*fleet.CertificateAuthority, 0, len(ops.Add)+len(ops.Update))
		upserts = append(upserts, ops.Add...)
		upserts = append(upserts, ops.Update...)
		g, err := fleet.GroupCertificateAuthoritiesByType(upserts)
		if err != nil {
			return err
		}
		storedCAs = *g
		return nil
	}

	ds.GetGroupedCertificateAuthoritiesFunc = func(ctx context.Context, includeSecrets bool) (*fleet.GroupedCertificateAuthorities, error) {
		require.True(t, includeSecrets) // for gitops flows we expect all calls to include secrets

		muStoredCAs.Lock()
		defer muStoredCAs.Unlock()
		return &storedCAs, nil
	}

	ds.DeleteIconsAssociatedWithTitlesWithoutInstallersFunc = func(ctx context.Context, teamID uint) error {
		return nil
	}

	tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
	require.NoError(t, err)

	const (
		fleetServerURL = "https://fleet.example.com"
		orgName        = "GitOps Premium Test"
	)
	t.Setenv("FLEET_SERVER_URL", fleetServerURL)

	_, err = tmpFile.WriteString(
		`
controls:
  ios_updates:
    deadline: "2022-02-02"
    minimum_version: "17.6"
  ipados_updates:
    deadline: "2023-03-03"
    minimum_version: "18.0"
  enable_disk_encryption: true
  windows_require_bitlocker_pin: true
queries:
policies:
labels:
agent_options:
org_settings:
  certificate_authorities:
    ndes_scep_proxy:
      url: https://ndes.example.com/scep
      admin_url: https://ndes.example.com/admin
      username: ndes_user
      password: ndes_password
    digicert:
      - name: DigiCert
        url: https://one.digicert.com
        api_token: digicert_api_token
        profile_id: digicert_profile_id
        certificate_common_name: digicert_cn
        certificate_user_principal_names: ["digicert_upn"]
        certificate_seat_id: digicert_seat_id
      - name: DigiCert2
        url: https://two.digicert.com
        api_token: digicert_api_token2
        profile_id: digicert_profile_id2
        certificate_common_name: digicert_cn2
        certificate_seat_id: digicert_seat_id2
    custom_scep_proxy:
      - name: CustomScepProxy
        url: https://custom.scep.proxy.com
        challenge: challenge
      - name: CustomScepProxy2
        url: https://custom.scep.proxy.com2
        challenge: challenge2
  server_settings:
    server_url: $FLEET_SERVER_URL
  org_info:
    contact_url: https://example.com/contact
    org_logo_url: ""
    org_logo_url_light_background: ""
    org_name: ${ORG_NAME}
  secrets:
software:
`,
	)
	require.NoError(t, err)

	// Dry run
	t.Setenv("ORG_NAME", orgName)
	_ = RunAppForTest(t, []string{"gitops", "-f", tmpFile.Name(), "--dry-run"})
	assert.Equal(t, fleet.AppConfig{}, *savedAppConfig, "AppConfig should be empty")

	// Dry run will get existing CAs but won't apply anything
	assert.True(t, ds.GetGroupedCertificateAuthoritiesFuncInvoked)
	ds.GetGroupedCertificateAuthoritiesFuncInvoked = false
	assert.False(t, ds.BatchApplyCertificateAuthoritiesFuncInvoked)

	// Real run
	_ = RunAppForTest(t, []string{"gitops", "-f", tmpFile.Name()})
	assert.Equal(t, orgName, savedAppConfig.OrgInfo.OrgName)
	assert.Equal(t, fleetServerURL, savedAppConfig.ServerSettings.ServerURL)
	assert.Empty(t, enrolledSecrets)

	// GitOps should not overwrite GitOps UI Mode.
	assert.Equal(t, savedAppConfig.UIGitOpsMode.GitopsModeEnabled, true)
	assert.Equal(t, savedAppConfig.UIGitOpsMode.RepositoryURL, "https://didsomeonesaygitops.biz")

	// Check MDM settings
	require.True(t, savedAppConfig.MDM.EnableDiskEncryption.Value)
	require.True(t, savedAppConfig.MDM.RequireBitLockerPIN.Value)

	// Check certificate authorities
	assert.True(t, ds.GetGroupedCertificateAuthoritiesFuncInvoked)
	assert.True(t, ds.BatchApplyCertificateAuthoritiesFuncInvoked)
	muStoredCAs.Lock()
	defer muStoredCAs.Unlock()

	// check ndes
	require.NotNil(t, storedCAs.NDESSCEP)
	assert.Equal(t, "https://ndes.example.com/scep", storedCAs.NDESSCEP.URL)

	// check digicert
	assert.True(t, digiCertService.VerifyProfileIDFuncInvoked)
	assert.Len(t, storedCAs.DigiCert, 2)
	digicertByName := make(map[string]fleet.DigiCertCA, 2)
	for _, d := range storedCAs.DigiCert {
		digicertByName[d.Name] = d
	}
	d1, ok := digicertByName["DigiCert"]
	assert.True(t, ok)
	assert.Equal(t, "DigiCert", d1.Name)
	assert.Equal(t, "https://one.digicert.com", d1.URL)
	assert.Equal(t, "digicert_api_token", d1.APIToken)
	assert.Equal(t, "digicert_profile_id", d1.ProfileID)
	assert.Equal(t, "digicert_cn", d1.CertificateCommonName)
	assert.Equal(t, []string{"digicert_upn"}, d1.CertificateUserPrincipalNames)
	assert.Equal(t, "digicert_seat_id", d1.CertificateSeatID)
	d2, ok := digicertByName["DigiCert2"]
	assert.True(t, ok)
	assert.Equal(t, "DigiCert2", d2.Name)
	assert.Equal(t, "https://two.digicert.com", d2.URL)
	assert.Equal(t, "digicert_api_token2", d2.APIToken)
	assert.Equal(t, "digicert_profile_id2", d2.ProfileID)
	assert.Equal(t, "digicert_cn2", d2.CertificateCommonName)
	assert.Empty(t, d2.CertificateUserPrincipalNames)
	assert.Equal(t, "digicert_seat_id2", d2.CertificateSeatID)

	// check custom SCEP proxies
	assert.Len(t, storedCAs.CustomScepProxy, 2)
	customSCEPByName := make(map[string]fleet.CustomSCEPProxyCA, 2)
	for _, scep := range storedCAs.CustomScepProxy {
		customSCEPByName[scep.Name] = scep
	}
	cs1, ok := customSCEPByName["CustomScepProxy"]
	assert.True(t, ok)
	assert.Equal(t, "CustomScepProxy", cs1.Name)
	assert.Equal(t, "https://custom.scep.proxy.com", cs1.URL)
	assert.Equal(t, "challenge", cs1.Challenge)
	cs2, ok := customSCEPByName["CustomScepProxy2"]
	assert.True(t, ok)
	assert.Equal(t, "CustomScepProxy2", cs2.Name)
	assert.Equal(t, "https://custom.scep.proxy.com2", cs2.URL)
	assert.Equal(t, "challenge2", cs2.Challenge)

	// // TODO(hca): if/when we have mock hydrant service, add yaml below to tmpFile and include
	// // following tests
	// // ```yaml
	// // hydrant:
	// //   - name: Hydrant
	// //     url: https://hydrant.example.com
	// //     client_id: hydrant_id
	// //     client_secret: hydrant_secret
	// //   - name: Hydrant2
	// //     url: https://hydrant2.example.com
	// //     client_id: hydrant2_id
	// //     client_secret: hydrant2_secret
	// // ````

	// // check hydrant
	// assert.Len(t, storedCAs.Hydrant, 2)
	// hydrantByName := make(map[string]fleet.HydrantCA, 2)
	// for _, h := range storedCAs.Hydrant {
	// 	hydrantByName[h.Name] = h
	// }
	// h1, ok := hydrantByName["Hydrant"]
	// assert.True(t, ok)
	// assert.Equal(t, "Hydrant", h1.Name)
	// assert.Equal(t, "https://hydrant.example.com", h1.URL)
	// assert.Equal(t, "hydrant_id", h1.ClientID)
	// assert.Equal(t, "hydrant_secret", h1.ClientSecret)
	// h2, ok := hydrantByName["Hydrant2"]
	// assert.True(t, ok)
	// assert.Equal(t, "Hydrant2", h2.Name)
	// assert.Equal(t, "https://hydrant2.example.com", h2.URL)
	// assert.Equal(t, "hydrant2_id", h2.ClientID)
	// assert.Equal(t, "hydrant2_secret", h2.ClientSecret)
}

func TestGitOpsBasicTeam(t *testing.T) {
	// Cannot run t.Parallel() because it sets environment variables
	license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
	_, ds := testing_utils.RunServerWithMockedDS(
		t, &service.TestServerOpts{
			License:       license,
			KeyValueStore: testing_utils.NewMemKeyValueStore(),
		},
	)

	const secret = "TestSecret"

	ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
		return nil
	}
	ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
		return nil
	}
	ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) ([]fleet.ScriptResponse, error) {
		return []fleet.ScriptResponse{}, nil
	}
	ds.BatchSetMDMProfilesFunc = func(
		ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, androidProfiles []*fleet.MDMAndroidConfigProfile, vars []fleet.MDMProfileIdentifierFleetVariables,
	) (updates fleet.MDMProfilesUpdates, err error) {
		return fleet.MDMProfilesUpdates{}, nil
	}
	ds.BulkSetPendingMDMHostProfilesFunc = func(
		ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
	) (updates fleet.MDMProfilesUpdates, err error) {
		return fleet.MDMProfilesUpdates{}, nil
	}
	ds.NewActivityFunc = func(
		ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
	) error {
		return nil
	}
	ds.ListTeamPoliciesFunc = func(
		ctx context.Context, teamID uint, opts fleet.ListOptions, iopts fleet.ListOptions,
	) (teamPolicies []*fleet.Policy, inheritedPolicies []*fleet.Policy, err error) {
		return nil, nil, nil
	}
	ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
		return nil, 0, nil, nil
	}
	ds.GetLabelSpecsFunc = func(ctx context.Context) ([]*fleet.LabelSpec, error) {
		return nil, nil
	}
	ds.DeleteIconsAssociatedWithTitlesWithoutInstallersFunc = func(ctx context.Context, teamID uint) error {
		return nil
	}

	// Mock DefaultTeamConfig functions for No Team webhook settings
	setupDefaultTeamConfigMocks(ds)

	team := &fleet.Team{
		ID:        1,
		CreatedAt: time.Now(),
		Name:      teamName,
	}
	var savedTeam *fleet.Team
	ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
		if name == teamName && savedTeam != nil {
			return savedTeam, nil
		}
		return nil, &notFoundError{}
	}
	ds.TeamByFilenameFunc = func(ctx context.Context, filename string) (*fleet.Team, error) {
		if savedTeam != nil && *savedTeam.Filename == filename {
			return savedTeam, nil
		}
		return nil, &notFoundError{}
	}
	// Track default team config for team 0
	defaultTeamConfig := &fleet.TeamConfig{}

	ds.TeamLiteFunc = func(ctx context.Context, tid uint) (*fleet.TeamLite, error) {
		if tid == 0 {
			// Return a mock team 0 with the default config
			return &fleet.TeamLite{
				ID:     0,
				Name:   fleet.ReservedNameNoTeam,
				Config: defaultTeamConfig.ToLite(),
			}, nil
		}
		if tid == team.ID {
			return savedTeam.ToTeamLite(), nil
		}
		return nil, nil
	}

	ds.DefaultTeamConfigFunc = func(ctx context.Context) (*fleet.TeamConfig, error) {
		return defaultTeamConfig, nil
	}

	ds.SaveDefaultTeamConfigFunc = func(ctx context.Context, config *fleet.TeamConfig) error {
		defaultTeamConfig = config
		return nil
	}
	var enrolledTeamSecrets []*fleet.EnrollSecret
	ds.NewTeamFunc = func(ctx context.Context, newTeam *fleet.Team) (*fleet.Team, error) {
		newTeam.ID = team.ID
		savedTeam = newTeam
		enrolledTeamSecrets = newTeam.Secrets
		return newTeam, nil
	}
	ds.IsEnrollSecretAvailableFunc = func(ctx context.Context, secret string, isNew bool, teamID *uint) (bool, error) {
		return true, nil
	}
	ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
		savedTeam = team
		return team, nil
	}
	ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
		require.Len(t, labels, 1)
		switch labels[0] {
		case fleet.BuiltinLabelMacOS14Plus:
			return map[string]uint{fleet.BuiltinLabelMacOS14Plus: 1}, nil
		case fleet.BuiltinLabelIOS:
			return map[string]uint{fleet.BuiltinLabelIOS: 2}, nil
		case fleet.BuiltinLabelIPadOS:
			return map[string]uint{fleet.BuiltinLabelIPadOS: 3}, nil
		default:
			return nil, &notFoundError{}
		}
	}
	ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error {
		return nil
	}
	ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
		return nil
	}
	ds.BatchSetInHouseAppsInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
		return nil
	}
	ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
		return nil, nil
	}
	ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
		enrolledTeamSecrets = secrets
		return nil
	}
	ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
		return &fleet.MDMAppleDeclaration{}, nil
	}
	ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
		return &fleet.Job{}, nil
	}
	ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
		return nil, 0, nil, nil
	}
	ds.DeleteSetupExperienceScriptFunc = func(ctx context.Context, teamID *uint) error {
		return nil
	}

	ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, options fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
		return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
	}

	ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
		return nil, nil
	}

	tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
	require.NoError(t, err)

	t.Setenv("TEST_SECRET", "")

	_, err = tmpFile.WriteString(
		`
controls:
  ios_updates:
    deadline: "2024-10-10"
    minimum_version: "18.0"
  ipados_updates:
    deadline: "2025-11-11"
    minimum_version: "17.6"
queries:
policies:
agent_options:
labels:
name: ${TEST_TEAM_NAME}
team_settings:
  secrets: ${TEST_SECRET}
software:
`,
	)
	require.NoError(t, err)

	// DoGitOps error
	t.Setenv("TEST_TEAM_NAME", "")
	_, err = RunAppNoChecks([]string{"gitops", "-f", tmpFile.Name()})
	require.Error(t, err)
	assert.Contains(t, err.Error(), "'name' is required")

	// Invalid name for "No team" file (dry and real).
	t.Setenv("TEST_TEAM_NAME", "no TEam")
	_, err = RunAppNoChecks([]string{"gitops", "-f", tmpFile.Name(), "--dry-run"})
	require.Error(t, err)
	assert.Contains(t, err.Error(), fmt.Sprintf("file %q for 'No team' must be named 'no-team.yml'", tmpFile.Name()))
	t.Setenv("TEST_TEAM_NAME", "no TEam")
	_, err = RunAppNoChecks([]string{"gitops", "-f", tmpFile.Name()})
	require.Error(t, err)
	assert.Contains(t, err.Error(), fmt.Sprintf("file %q for 'No team' must be named 'no-team.yml'", tmpFile.Name()))

	t.Setenv("TEST_TEAM_NAME", "All teams")
	_, err = RunAppNoChecks([]string{"gitops", "-f", tmpFile.Name(), "--dry-run"})
	require.Error(t, err)
	assert.Contains(t, err.Error(), `"All teams" is a reserved team name`)

	t.Setenv("TEST_TEAM_NAME", "All TEAMS")
	_, err = RunAppNoChecks([]string{"gitops", "-f", tmpFile.Name()})
	require.Error(t, err)
	assert.Contains(t, err.Error(), `"All teams" is a reserved team name`)

	// Dry run
	t.Setenv("TEST_TEAM_NAME", teamName)
	_ = RunAppForTest(t, []string{"gitops", "-f", tmpFile.Name(), "--dry-run"})
	assert.Nil(t, savedTeam)

	// Real run
	_ = RunAppForTest(t, []string{"gitops", "-f", tmpFile.Name()})
	require.NotNil(t, savedTeam)
	assert.Equal(t, teamName, savedTeam.Name)
	assert.Empty(t, enrolledTeamSecrets)
	assert.True(t, savedTeam.Config.Features.EnableSoftwareInventory)

	// The previous run created the team, so let's rerun with an existing team
	_ = RunAppForTest(t, []string{"gitops", "-f", tmpFile.Name()})
	assert.Empty(t, enrolledTeamSecrets)

	// Add a secret
	t.Setenv("TEST_SECRET", fmt.Sprintf("[{\"secret\":\"%s\"}]", secret))
	_ = RunAppForTest(t, []string{"gitops", "-f", tmpFile.Name()})
	require.Len(t, enrolledTeamSecrets, 1)
	assert.Equal(t, secret, enrolledTeamSecrets[0].Secret)
}

func TestGitOpsFullGlobal(t *testing.T) {
	// Cannot run t.Parallel() because it sets environment variables
	// mdm test configuration must be set so that activating windows MDM works.
	testCert, testKey, err := apple_mdm.NewSCEPCACertKey()
	require.NoError(t, err)
	testCertPEM := tokenpki.PEMCertificate(testCert.Raw)
	testKeyPEM := tokenpki.PEMRSAPrivateKey(testKey)
	fleetCfg := config.TestConfig()
	config.SetTestMDMConfig(t, &fleetCfg, testCertPEM, testKeyPEM, "../../../server/service/testdata")

	// License is not needed because we are not using any premium features in our config.
	_, ds := testing_utils.RunServerWithMockedDS(
		t, &service.TestServerOpts{
			MDMStorage:  new(mdmmock.MDMAppleStore),
			MDMPusher:   testing_utils.MockPusher{},
			FleetConfig: &fleetCfg,
		},
	)

	var appliedScripts []*fleet.Script
	ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) ([]fleet.ScriptResponse, error) {
		appliedScripts = scripts
		var scriptResponses []fleet.ScriptResponse
		for _, script := range scripts {
			scriptResponses = append(scriptResponses, fleet.ScriptResponse{
				ID:     script.ID,
				Name:   script.Name,
				TeamID: script.TeamID,
			})
		}

		return scriptResponses, nil
	}
	ds.NewActivityFunc = func(
		ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
	) error {
		return nil
	}
	var appliedMacProfiles []*fleet.MDMAppleConfigProfile
	var appliedWinProfiles []*fleet.MDMWindowsConfigProfile
	ds.BatchSetMDMProfilesFunc = func(
		ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, androidProfiles []*fleet.MDMAndroidConfigProfile, vars []fleet.MDMProfileIdentifierFleetVariables,
	) (updates fleet.MDMProfilesUpdates, err error) {
		appliedMacProfiles = macProfiles
		appliedWinProfiles = winProfiles
		return fleet.MDMProfilesUpdates{}, nil
	}
	ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string,
	) (updates fleet.MDMProfilesUpdates, err error) {
		return fleet.MDMProfilesUpdates{}, nil
	}
	ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
		return job, nil
	}
	ds.DeleteIconsAssociatedWithTitlesWithoutInstallersFunc = func(ctx context.Context, teamID uint) error {
		return nil
	}

	// Policies
	policy := fleet.Policy{}
	policy.ID = 1
	policy.Name = "Policy to delete"
	policyDeleted := false
	ds.ListTeamPoliciesFunc = func(
		ctx context.Context, teamID uint, opts fleet.ListOptions, iopts fleet.ListOptions,
	) (teamPolicies []*fleet.Policy, inheritedPolicies []*fleet.Policy, err error) {
		return nil, nil, nil
	}
	ds.ListGlobalPoliciesFunc = func(ctx context.Context, opts fleet.ListOptions) ([]*fleet.Policy, error) {
		return []*fleet.Policy{&policy}, nil
	}
	ds.PoliciesByIDFunc = func(ctx context.Context, ids []uint) (map[uint]*fleet.Policy, error) {
		if slices.Contains(ids, 1) {
			return map[uint]*fleet.Policy{1: &policy}, nil
		}
		return nil, nil
	}
	ds.DeleteGlobalPoliciesFunc = func(ctx context.Context, ids []uint) ([]uint, error) {
		policyDeleted = true
		assert.Equal(t, []uint{policy.ID}, ids)
		return ids, nil
	}
	var appliedPolicySpecs []*fleet.PolicySpec
	ds.ApplyPolicySpecsFunc = func(ctx context.Context, authorID uint, specs []*fleet.PolicySpec) error {
		appliedPolicySpecs = specs
		return nil
	}

	// Queries
	query := fleet.Query{}
	query.ID = 1
	query.Name = "Query to delete"
	queryDeleted := false
	ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
		return []*fleet.Query{&query}, 1, nil, nil
	}
	ds.DeleteQueriesFunc = func(ctx context.Context, ids []uint) (uint, error) {
		queryDeleted = true
		assert.Equal(t, []uint{query.ID}, ids)
		return 1, nil
	}
	ds.QueryFunc = func(ctx context.Context, id uint) (*fleet.Query, error) {
		if id == query.ID {
			return &query, nil
		}
		return nil, nil
	}
	var appliedQueries []*fleet.Query
	ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string) (*fleet.Query, error) {
		return nil, &notFoundError{}
	}
	ds.ApplyQueriesFunc = func(
		ctx context.Context, authorID uint, queries []*fleet.Query, queriesToDiscardResults map[uint]struct{},
	) error {
		appliedQueries = queries
		return nil
	}

	var appliedLabelSpecs []*fleet.LabelSpec
	var deletedLabels []string
	ds.GetLabelSpecsFunc = func(ctx context.Context) ([]*fleet.LabelSpec, error) {
		return []*fleet.LabelSpec{
			{
				Name:                "a",
				Description:         "A global label",
				LabelMembershipType: fleet.LabelMembershipTypeManual,
				Hosts:               []string{"host2", "host3"},
			},
			{
				Name:                "c",
				Description:         "A label that should be deleted",
				LabelMembershipType: fleet.LabelMembershipTypeDynamic,
				Query:               "SELECT 1 from osquery_info",
			},
		}, nil
	}
	ds.ApplyLabelSpecsWithAuthorFunc = func(ctx context.Context, specs []*fleet.LabelSpec, authorID *uint) (err error) {
		appliedLabelSpecs = specs
		return nil
	}

	ds.DeleteLabelFunc = func(ctx context.Context, name string) error {
		deletedLabels = append(deletedLabels, name)
		return nil
	}

	ds.LabelsByNameFunc = func(ctx context.Context, names []string) (map[string]*fleet.Label, error) {
		return map[string]*fleet.Label{
			"a": {
				ID:   1,
				Name: "a",
			},
			"b": {
				ID:   2,
				Name: "b",
			},
		}, nil
	}

	// Mock appConfig
	savedAppConfig := &fleet.AppConfig{}
	ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
		return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}, nil
	}
	ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
		savedAppConfig = config
		return nil
	}
	ds.IsEnrollSecretAvailableFunc = func(ctx context.Context, secret string, isNew bool, teamID *uint) (bool, error) {
		return true, nil
	}
	var enrolledSecrets []*fleet.EnrollSecret
	ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
		enrolledSecrets = secrets
		return nil
	}

	// Needed for checking tokens
	ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
		return nil
	}
	ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
		return []*fleet.VPPTokenDB{}, nil
	}
	ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
		return []*fleet.ABMToken{}, nil
	}

	ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) {
		return document, nil, nil
	}

	const (
		fleetServerURL = "https://fleet.example.com"
		orgName        = "GitOps Test"
	)
	t.Setenv("FLEET_SERVER_URL", fleetServerURL)
	t.Setenv("ORG_NAME", orgName)
	t.Setenv("SOFTWARE_INSTALLER_URL", fleetServerURL)

	// Dry run w/ top-level labels key
	logs := RunAppForTest(t, []string{"gitops", "-f", "./testdata/gitops/global_config_no_paths.yml", "--dry-run"})
	fmt.Printf("%s", logs)
	fmt.Printf("-----------\n")
	assert.Equal(t, fleet.AppConfig{}, *savedAppConfig, "AppConfig should be empty")
	assert.Len(t, enrolledSecrets, 0)
	assert.Len(t, appliedPolicySpecs, 0)
	assert.Len(t, appliedQueries, 0)
	assert.Len(t, appliedScripts, 0)
	assert.Len(t, appliedMacProfiles, 0)
	assert.Len(t, appliedWinProfiles, 0)
	assert.Len(t, appliedLabelSpecs, 0)
	assert.Len(t, deletedLabels, 0)

	// Dry run w/out top-level labels key
	logs = RunAppForTest(t, []string{"gitops", "-f", "./testdata/gitops/global_config_no_paths_no_labels.yml", "--dry-run"})
	fmt.Printf("%s", logs)
	fmt.Printf("-----------\n")
	assert.Equal(t, fleet.AppConfig{}, *savedAppConfig, "AppConfig should be empty")
	assert.Len(t, enrolledSecrets, 0)
	assert.Len(t, appliedPolicySpecs, 0)
	assert.Len(t, appliedQueries, 0)
	assert.Len(t, appliedScripts, 0)
	assert.Len(t, appliedMacProfiles, 0)
	assert.Len(t, appliedWinProfiles, 0)
	assert.Len(t, appliedLabelSpecs, 0)
	assert.Len(t, deletedLabels, 0)

	// Real run w/ top-level labels key
	logs = RunAppForTest(t, []string{"gitops", "-f", "./testdata/gitops/global_config_no_paths.yml"})
	fmt.Printf("%s", logs)
	fmt.Printf("-----------\n")
	assert.Equal(t, orgName, savedAppConfig.OrgInfo.OrgName)
	assert.Equal(t, fleetServerURL, savedAppConfig.ServerSettings.ServerURL)
	assert.Contains(t, string(*savedAppConfig.AgentOptions), "distributed_denylist_duration")
	assert.Equal(t, 2000, savedAppConfig.ServerSettings.QueryReportCap)
	assert.Len(t, enrolledSecrets, 2)
	assert.True(t, policyDeleted)
	assert.Len(t, appliedPolicySpecs, 5)
	assert.Len(t, appliedPolicySpecs[0].LabelsIncludeAny, 1)
	assert.Len(t, appliedPolicySpecs[0].LabelsExcludeAny, 0)
	assert.Equal(t, appliedPolicySpecs[0].LabelsIncludeAny[0], "a")
	assert.Len(t, appliedPolicySpecs[1].LabelsIncludeAny, 0)
	assert.Len(t, appliedPolicySpecs[1].LabelsExcludeAny, 1)
	assert.Equal(t, appliedPolicySpecs[1].LabelsExcludeAny[0], "b")

	assert.True(t, queryDeleted)
	assert.Len(t, appliedQueries, 3)
	assert.Len(t, appliedScripts, 1)
	assert.Len(t, appliedMacProfiles, 1)
	assert.Len(t, appliedWinProfiles, 1)
	require.Len(t, savedAppConfig.Integrations.GoogleCalendar, 1)
	assert.Equal(t, "service@example.com", savedAppConfig.Integrations.GoogleCalendar[0].ApiKey["client_email"])
	assert.True(t, savedAppConfig.ActivityExpirySettings.ActivityExpiryEnabled)
	assert.Equal(t, 60, savedAppConfig.ActivityExpirySettings.ActivityExpiryWindow)
	assert.True(t, savedAppConfig.ServerSettings.AIFeaturesDisabled)
	assert.True(t, savedAppConfig.WebhookSettings.ActivitiesWebhook.Enable)
	assert.Equal(t, "https://activities_webhook_url", savedAppConfig.WebhookSettings.ActivitiesWebhook.DestinationURL)
	assert.Len(t, appliedLabelSpecs, 2)
	assert.Len(t, deletedLabels, 1)
	assert.Len(t, appliedQueries[0].LabelsIncludeAny, 2)
	assert.Contains(t, []string{appliedQueries[0].LabelsIncludeAny[0].LabelName, appliedQueries[0].LabelsIncludeAny[1].LabelName}, "a")
	assert.Contains(t, []string{appliedQueries[0].LabelsIncludeAny[0].LabelName, appliedQueries[0].LabelsIncludeAny[1].LabelName}, "b")

	// Reset labels arrays
	deletedLabels = make([]string, 0)
	appliedLabelSpecs = make([]*fleet.LabelSpec, 0)
	// Real run w/out top-level labels key
	logs = RunAppForTest(t, []string{"gitops", "-f", "./testdata/gitops/global_config_no_paths_no_labels.yml"})
	fmt.Printf("%s", logs)
	assert.Len(t, appliedLabelSpecs, 0)
	assert.Len(t, deletedLabels, 0)
}

func TestGitOpsFullTeam(t *testing.T) {
	// Cannot run t.Parallel() because it sets environment variables
	license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}

	// mdm test configuration must be set so that activating windows MDM works.
	testCert, testKey, err := apple_mdm.NewSCEPCACertKey()
	require.NoError(t, err)
	testCertPEM := tokenpki.PEMCertificate(testCert.Raw)
	testKeyPEM := tokenpki.PEMRSAPrivateKey(testKey)
	fleetCfg := config.TestConfig()
	config.SetTestMDMConfig(t, &fleetCfg, testCertPEM, testKeyPEM, "../../../server/service/testdata")

	// License is not needed because we are not using any premium features in our config.
	_, ds := testing_utils.RunServerWithMockedDS(
		t, &service.TestServerOpts{
			License:          license,
			MDMStorage:       new(mdmmock.MDMAppleStore),
			MDMPusher:        testing_utils.MockPusher{},
			FleetConfig:      &fleetCfg,
			NoCacheDatastore: true,
			KeyValueStore:    testing_utils.NewMemKeyValueStore(),
		},
	)

	appConfig := fleet.AppConfig{
		// During dry run, the global calendar integration setting may not be set
		MDM: fleet.MDM{
			EnabledAndConfigured:        true,
			WindowsEnabledAndConfigured: true,
		},
	}
	ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
		return &appConfig, nil
	}

	var appliedScripts []*fleet.Script
	ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) ([]fleet.ScriptResponse, error) {
		appliedScripts = scripts
		var scriptResponses []fleet.ScriptResponse
		for _, script := range scripts {
			scriptResponses = append(scriptResponses, fleet.ScriptResponse{
				ID:     script.ID,
				Name:   script.Name,
				TeamID: script.TeamID,
			})
		}

		return scriptResponses, nil
	}
	ds.NewActivityFunc = func(
		ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
	) error {
		return nil
	}
	var appliedMacProfiles []*fleet.MDMAppleConfigProfile
	var appliedWinProfiles []*fleet.MDMWindowsConfigProfile
	ds.BatchSetMDMProfilesFunc = func(
		ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, androidProfiles []*fleet.MDMAndroidConfigProfile, vars []fleet.MDMProfileIdentifierFleetVariables,
	) (updates fleet.MDMProfilesUpdates, err error) {
		appliedMacProfiles = macProfiles
		appliedWinProfiles = winProfiles
		return fleet.MDMProfilesUpdates{}, nil
	}
	ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string,
	) (updates fleet.MDMProfilesUpdates, err error) {
		return fleet.MDMProfilesUpdates{}, nil
	}
	ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
		return job, nil
	}
	ds.NewMDMAppleConfigProfileFunc = func(ctx context.Context, profile fleet.MDMAppleConfigProfile, vars []fleet.FleetVarName) (*fleet.MDMAppleConfigProfile, error) {
		return &profile, nil
	}
	ds.NewMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
		return declaration, nil
	}
	ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
		require.ElementsMatch(t, labels, []string{fleet.BuiltinLabelMacOS14Plus})
		return map[string]uint{fleet.BuiltinLabelMacOS14Plus: 1}, nil
	}
	ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
		declaration.DeclarationUUID = uuid.NewString()
		return declaration, nil
	}
	ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error {
		return nil
	}
	ds.GetMDMAppleBootstrapPackageMetaFunc = func(ctx context.Context, teamID uint) (*fleet.MDMAppleBootstrapPackage, error) {
		return &fleet.MDMAppleBootstrapPackage{}, nil
	}
	ds.DeleteMDMAppleBootstrapPackageFunc = func(ctx context.Context, teamID uint) error {
		return nil
	}
	ds.GetMDMAppleSetupAssistantFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMAppleSetupAssistant, error) {
		return nil, nil
	}
	ds.DeleteMDMAppleSetupAssistantFunc = func(ctx context.Context, teamID *uint) error {
		return nil
	}
	ds.GetSoftwareCategoryIDsFunc = func(ctx context.Context, names []string) ([]uint, error) {
		return []uint{}, nil
	}
	ds.DeleteIconsAssociatedWithTitlesWithoutInstallersFunc = func(ctx context.Context, teamID uint) error {
		return nil
	}

	ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, options fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
		return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
	}

	ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
		return nil, nil
	}

	// Mock DefaultTeamConfig functions for No Team webhook settings
	setupDefaultTeamConfigMocks(ds)

	// Team
	var savedTeam *fleet.Team
	ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
		if name == "Conflict" {
			return &fleet.Team{}, nil
		}
		if savedTeam != nil && savedTeam.Name == name {
			return savedTeam, nil
		}
		return nil, &notFoundError{}
	}
	ds.TeamByFilenameFunc = func(ctx context.Context, filename string) (*fleet.Team, error) {
		if savedTeam != nil && *savedTeam.Filename == filename {
			return savedTeam, nil
		}
		return nil, &notFoundError{}
	}
	ds.TeamWithExtrasFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
		if tid == savedTeam.ID {
			return savedTeam, nil
		}
		return nil, nil
	}
	ds.TeamLiteFunc = func(ctx context.Context, tid uint) (*fleet.TeamLite, error) {
		if tid == savedTeam.ID {
			return savedTeam.ToTeamLite(), nil
		}
		return nil, nil
	}
	ds.IsEnrollSecretAvailableFunc = func(ctx context.Context, secret string, isNew bool, teamID *uint) (bool, error) {
		return true, nil
	}
	const teamID = uint(123)
	var enrolledSecrets []*fleet.EnrollSecret
	ds.NewTeamFunc = func(ctx context.Context, newTeam *fleet.Team) (*fleet.Team, error) {
		newTeam.ID = teamID
		savedTeam = newTeam
		enrolledSecrets = newTeam.Secrets
		return newTeam, nil
	}
	ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
		if team.ID == teamID {
			savedTeam = team
		} else {
			assert.Fail(t, "unexpected team ID when saving team")
		}
		return team, nil
	}

	ds.GetTeamsWithInstallerByHashFunc = func(ctx context.Context, sha256, url string) (map[uint][]*fleet.ExistingSoftwareInstaller, error) {
		return map[uint][]*fleet.ExistingSoftwareInstaller{}, nil
	}

	// Policies
	policy := fleet.Policy{}
	policy.ID = 1
	policy.Name = "Policy to delete"
	policy.TeamID = ptr.Uint(teamID)
	policyDeleted := false
	ds.ListTeamPoliciesFunc = func(
		ctx context.Context, teamID uint, opts fleet.ListOptions, iopts fleet.ListOptions,
	) (teamPolicies []*fleet.Policy, inheritedPolicies []*fleet.Policy, err error) {
		if teamID != 0 {
			return []*fleet.Policy{&policy}, nil, nil
		}
		return nil, nil, nil
	}
	ds.PoliciesByIDFunc = func(ctx context.Context, ids []uint) (map[uint]*fleet.Policy, error) {
		if slices.Contains(ids, 1) {
			return map[uint]*fleet.Policy{1: &policy}, nil
		}
		return nil, nil
	}
	ds.DeleteTeamPoliciesFunc = func(ctx context.Context, teamID uint, IDs []uint) ([]uint, error) {
		policyDeleted = true
		assert.Equal(t, []uint{policy.ID}, IDs)
		return []uint{policy.ID}, nil
	}
	var appliedPolicySpecs []*fleet.PolicySpec
	ds.ApplyPolicySpecsFunc = func(ctx context.Context, authorID uint, specs []*fleet.PolicySpec) error {
		appliedPolicySpecs = specs
		return nil
	}

	ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) {
		return document, nil, nil
	}

	// Queries
	query := fleet.Query{}
	query.ID = 1
	query.TeamID = ptr.Uint(teamID)
	query.Name = "Query to delete"
	queryDeleted := false
	ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
		return []*fleet.Query{&query}, 1, nil, nil
	}
	ds.DeleteQueriesFunc = func(ctx context.Context, ids []uint) (uint, error) {
		queryDeleted = true
		assert.Equal(t, []uint{query.ID}, ids)
		return 1, nil
	}
	ds.QueryFunc = func(ctx context.Context, id uint) (*fleet.Query, error) {
		if id == query.ID {
			return &query, nil
		}
		return nil, nil
	}
	var appliedQueries []*fleet.Query
	ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string) (*fleet.Query, error) {
		return nil, &notFoundError{}
	}
	ds.ApplyQueriesFunc = func(
		ctx context.Context, authorID uint, queries []*fleet.Query, queriesToDiscardResults map[uint]struct{},
	) error {
		appliedQueries = queries
		return nil
	}

	testing_utils.AddLabelMocks(ds)

	var appliedSoftwareInstallers []*fleet.UploadSoftwareInstallerPayload
	ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
		if teamID != nil && *teamID != 0 {
			appliedSoftwareInstallers = installers
		}
		return nil
	}
	ds.BatchSetInHouseAppsInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
		return nil
	}
	ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
		return nil, nil
	}
	ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
		return nil
	}
	ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
		return nil
	}
	ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
		enrolledSecrets = secrets
		return nil
	}
	ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
		return nil, 0, nil, nil
	}
	ds.DeleteSetupExperienceScriptFunc = func(ctx context.Context, teamID *uint) error {
		return nil
	}

	testing_utils.StartSoftwareInstallerServer(t)

	t.Setenv("TEST_TEAM_NAME", teamName)
	t.Setenv("ENABLE_DISK_ENCRYPTION", "true")
	t.Setenv("WINDOWS_REQUIRE_BITLOCKER_PIN", "true")

	// Dry run
	const baseFilename = "team_config_no_paths.yml"
	gitopsFile := "./testdata/gitops/" + baseFilename
	_ = RunAppForTest(t, []string{"gitops", "-f", gitopsFile, "--dry-run"})
	assert.Nil(t, savedTeam)
	assert.Len(t, enrolledSecrets, 0)
	assert.Len(t, appliedPolicySpecs, 0)
	assert.Len(t, appliedQueries, 0)
	assert.Len(t, appliedScripts, 0)
	assert.Len(t, appliedMacProfiles, 0)
	assert.Len(t, appliedWinProfiles, 0)
	assert.Empty(t, appliedSoftwareInstallers)

	// Real run
	// Setting global calendar config
	appConfig.Integrations = fleet.Integrations{
		GoogleCalendar: []*fleet.GoogleCalendarIntegration{{}},
	}
	_ = RunAppForTest(t, []string{"gitops", "-f", gitopsFile})
	require.NotNil(t, savedTeam)
	assert.Equal(t, teamName, savedTeam.Name)
	assert.Contains(t, string(*savedTeam.Config.AgentOptions), "distributed_denylist_duration")
	assert.True(t, savedTeam.Config.Features.EnableHostUsers)
	assert.Equal(t, 30, savedTeam.Config.HostExpirySettings.HostExpiryWindow)
	assert.True(t, savedTeam.Config.MDM.EnableDiskEncryption)
	assert.True(t, savedTeam.Config.MDM.RequireBitLockerPIN)
	assert.Len(t, enrolledSecrets, 2)
	assert.True(t, policyDeleted)
	assert.Len(t, appliedPolicySpecs, 5)
	assert.True(t, queryDeleted)
	assert.Len(t, appliedQueries, 3)
	assert.Len(t, appliedScripts, 1)
	assert.Len(t, appliedMacProfiles, 1)
	assert.Len(t, appliedWinProfiles, 1)
	assert.True(t, savedTeam.Config.WebhookSettings.HostStatusWebhook.Enable)
	assert.Equal(t, "https://example.com/host_status_webhook", savedTeam.Config.WebhookSettings.HostStatusWebhook.DestinationURL)
	require.NotNil(t, savedTeam.Config.Integrations.GoogleCalendar)
	assert.True(t, savedTeam.Config.Integrations.GoogleCalendar.Enable)
	assert.Equal(t, baseFilename, *savedTeam.Filename)
	require.Len(t, appliedSoftwareInstallers, 2)
	packageID := `"ruby"`
	uninstallScriptProcessed := strings.ReplaceAll(file.GetUninstallScript("deb"), "$PACKAGE_ID", packageID)
	assert.ElementsMatch(t, []string{fmt.Sprintf("echo 'uninstall' %s\n", packageID), uninstallScriptProcessed},
		[]string{appliedSoftwareInstallers[0].UninstallScript, appliedSoftwareInstallers[1].UninstallScript})

	// Change disk encryption settings
	t.Setenv("ENABLE_DISK_ENCRYPTION", "false")
	t.Setenv("WINDOWS_REQUIRE_BITLOCKER_PIN", "false")
	_ = RunAppForTest(t, []string{"gitops", "-f", gitopsFile, "--dry-run"})
	_ = RunAppForTest(t, []string{"gitops", "-f", gitopsFile})
	require.NotNil(t, savedTeam)
	assert.False(t, savedTeam.Config.MDM.EnableDiskEncryption)
	assert.False(t, savedTeam.Config.MDM.RequireBitLockerPIN)

	// Change team name
	newTeamName := "New Team Name"
	t.Setenv("TEST_TEAM_NAME", newTeamName)
	_ = RunAppForTest(t, []string{"gitops", "-f", gitopsFile, "--dry-run"})
	_ = RunAppForTest(t, []string{"gitops", "-f", gitopsFile})
	require.NotNil(t, savedTeam)
	assert.Equal(t, newTeamName, savedTeam.Name)
	assert.Equal(t, baseFilename, *savedTeam.Filename)

	// Try to change team name again, but this time the new name conflicts with an existing team
	t.Setenv("TEST_TEAM_NAME", "Conflict")
	_, err = RunAppNoChecks([]string{"gitops", "-f", gitopsFile, "--dry-run"})
	assert.ErrorContains(t, err, "team name already exists")
	_, err = RunAppNoChecks([]string{"gitops", "-f", gitopsFile})
	assert.ErrorContains(t, err, "team name already exists")

	// Now clear the settings
	t.Setenv("TEST_TEAM_NAME", newTeamName)
	tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
	require.NoError(t, err)
	secret := "TestSecret"
	t.Setenv("TEST_SECRET", secret)

	_, err = tmpFile.WriteString(
		`
controls:
queries:
policies:
agent_options:
name: ${TEST_TEAM_NAME}
team_settings:
  secrets: [{"secret":"${TEST_SECRET}"}]
software:
`,
	)
	require.NoError(t, err)

	// Dry run
	savedTeam = nil
	_ = RunAppForTest(t, []string{"gitops", "-f", tmpFile.Name(), "--dry-run"})
	assert.Nil(t, savedTeam)

	// Real run
	_ = RunAppForTest(t, []string{"gitops", "-f", tmpFile.Name()})
	require.NotNil(t, savedTeam)
	assert.Equal(t, newTeamName, savedTeam.Name)
	require.Len(t, enrolledSecrets, 1)
	assert.Equal(t, secret, enrolledSecrets[0].Secret)
	assert.False(t, savedTeam.Config.WebhookSettings.HostStatusWebhook.Enable)
	assert.Equal(t, "", savedTeam.Config.WebhookSettings.HostStatusWebhook.DestinationURL)
	assert.NotNil(t, savedTeam.Config.Integrations.GoogleCalendar)
	assert.False(t, savedTeam.Config.Integrations.GoogleCalendar.Enable)
	assert.Empty(t, savedTeam.Config.Integrations.GoogleCalendar)
	assert.Empty(t, savedTeam.Config.MDM.MacOSSettings.CustomSettings)
	assert.Empty(t, savedTeam.Config.MDM.WindowsSettings.CustomSettings.Value)
	assert.Empty(t, savedTeam.Config.MDM.MacOSUpdates.Deadline.Value)
	assert.Empty(t, savedTeam.Config.MDM.MacOSUpdates.MinimumVersion.Value)
	assert.Empty(t, savedTeam.Config.MDM.MacOSSetup.BootstrapPackage.Value)
	assert.False(t, savedTeam.Config.MDM.EnableDiskEncryption)
	assert.Equal(t, filepath.Base(tmpFile.Name()), *savedTeam.Filename)
}

func createFakeITunesAndVPPServices(t *testing.T) {
	config := &testing_utils.AppleVPPConfigSrvConf{
		Assets: []vpp.Asset{
			{
				AdamID:         "1",
				PricingParam:   "STDQ",
				AvailableCount: 12,
			},
			{
				AdamID:         "2",
				PricingParam:   "STDQ",
				AvailableCount: 3,
			},
		},
		SerialNumbers: []string{"123", "456"},
	}
	testing_utils.StartVPPApplyServer(t, config)

	appleITunesSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// a map of apps we can respond with
		db := map[string]string{
			// macos app
			"1": `{"bundleId": "a-1", "artworkUrl512": "https://example.com/images/1", "version": "1.0.0", "trackName": "App 1", "TrackID": 1}`,
			// macos, ios, ipados app
			"2": `{"bundleId": "b-2", "artworkUrl512": "https://example.com/images/2", "version": "2.0.0", "trackName": "App 2", "TrackID": 2,
				"supportedDevices": ["MacDesktop-MacDesktop", "iPhone5s-iPhone5s", "iPadAir-iPadAir"] }`,
			// ipados app
			"3": `{"bundleId": "c-3", "artworkUrl512": "https://example.com/images/3", "version": "3.0.0", "trackName": "App 3", "TrackID": 3,
				"supportedDevices": ["iPadAir-iPadAir"] }`,
		}

		adamIDString := r.URL.Query().Get("id")
		adamIDs := strings.Split(adamIDString, ",")

		var objs []string
		for _, a := range adamIDs {
			objs = append(objs, db[a])
		}

		_, _ = w.Write([]byte(fmt.Sprintf(`{"results": [%s]}`, strings.Join(objs, ","))))
	}))
	t.Setenv("FLEET_DEV_ITUNES_URL", appleITunesSrv.URL)
}

func TestGitOpsBasicGlobalAndTeam(t *testing.T) {
	// Cannot run t.Parallel() because it sets environment variables
	license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
	_, ds := testing_utils.RunServerWithMockedDS(
		t, &service.TestServerOpts{
			License:       license,
			KeyValueStore: testing_utils.NewMemKeyValueStore(),
		},
	)

	// Mock appConfig
	savedAppConfig := &fleet.AppConfig{}
	ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
		appConfig := savedAppConfig.Copy()
		return appConfig, nil
	}
	ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
		savedAppConfig = config
		return nil
	}

	ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
		return nil
	}
	ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
		return nil
	}
	ds.GetVPPAppsFunc = func(ctx context.Context, teamID *uint) ([]fleet.VPPAppResponse, error) {
		return []fleet.VPPAppResponse{}, nil
	}

	const (
		fleetServerURL = "https://fleet.example.com"
		orgName        = "GitOps Test"
		secret         = "TestSecret"
	)
	var enrolledSecrets []*fleet.EnrollSecret
	var enrolledTeamSecrets []*fleet.EnrollSecret
	var savedTeam *fleet.Team
	team := &fleet.Team{
		ID:        1,
		CreatedAt: time.Now(),
		Name:      teamName,
	}

	ds.IsEnrollSecretAvailableFunc = func(ctx context.Context, secret string, isNew bool, teamID *uint) (bool, error) {
		return true, nil
	}
	ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
		if teamID == nil {
			enrolledSecrets = secrets
		} else {
			enrolledTeamSecrets = secrets
		}
		return nil
	}
	ds.BatchSetMDMProfilesFunc = func(
		ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
		macDecls []*fleet.MDMAppleDeclaration, androidProfiles []*fleet.MDMAndroidConfigProfile, vars []fleet.MDMProfileIdentifierFleetVariables,
	) (updates fleet.MDMProfilesUpdates, err error) {
		assert.Empty(t, macProfiles)
		assert.Empty(t, winProfiles)
		assert.Empty(t, macDecls)
		assert.Empty(t, androidProfiles)
		return fleet.MDMProfilesUpdates{}, nil
	}
	ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) ([]fleet.ScriptResponse, error) {
		assert.Empty(t, scripts)
		return []fleet.ScriptResponse{}, nil
	}
	ds.BulkSetPendingMDMHostProfilesFunc = func(
		ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
	) (updates fleet.MDMProfilesUpdates, err error) {
		assert.Empty(t, profileUUIDs)
		return fleet.MDMProfilesUpdates{}, nil
	}
	ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error {
		return nil
	}
	ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
		require.ElementsMatch(t, labels, []string{fleet.BuiltinLabelMacOS14Plus})
		return map[string]uint{fleet.BuiltinLabelMacOS14Plus: 1}, nil
	}
	ds.ListGlobalPoliciesFunc = func(ctx context.Context, opts fleet.ListOptions) ([]*fleet.Policy, error) { return nil, nil }
	ds.ListTeamPoliciesFunc = func(
		ctx context.Context, teamID uint, opts fleet.ListOptions, iopts fleet.ListOptions,
	) (teamPolicies []*fleet.Policy, inheritedPolicies []*fleet.Policy, err error) {
		return nil, nil, nil
	}
	ds.ListTeamsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.ListOptions) ([]*fleet.Team, error) {
		if savedTeam != nil {
			return []*fleet.Team{savedTeam}, nil
		}
		return nil, nil
	}
	ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
		return nil, 0, nil, nil
	}
	ds.DeleteIconsAssociatedWithTitlesWithoutInstallersFunc = func(ctx context.Context, teamID uint) error {
		return nil
	}

	testing_utils.AddLabelMocks(ds)

	// Mock DefaultTeamConfig functions for No Team webhook settings
	setupDefaultTeamConfigMocks(ds)

	ds.NewActivityFunc = func(
		ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
	) error {
		return nil
	}
	ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
		job.ID = 1
		return job, nil
	}
	// Track default team config for team 0
	defaultTeamConfig := &fleet.TeamConfig{}

	ds.TeamLiteFunc = func(ctx context.Context, tid uint) (*fleet.TeamLite, error) {
		if tid == 0 {
			// Return a mock team 0 with the default config
			return &fleet.TeamLite{
				ID:     0,
				Name:   fleet.ReservedNameNoTeam,
				Config: defaultTeamConfig.ToLite(),
			}, nil
		}
		if tid == team.ID {
			return savedTeam.ToTeamLite(), nil
		}
		return nil, nil
	}

	ds.DefaultTeamConfigFunc = func(ctx context.Context) (*fleet.TeamConfig, error) {
		return defaultTeamConfig, nil
	}

	ds.SaveDefaultTeamConfigFunc = func(ctx context.Context, config *fleet.TeamConfig) error {
		defaultTeamConfig = config
		return nil
	}
	ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
		if name == teamName && savedTeam != nil {
			return savedTeam, nil
		}
		return nil, &notFoundError{}
	}
	ds.TeamByFilenameFunc = func(ctx context.Context, filename string) (*fleet.Team, error) {
		if savedTeam != nil && *savedTeam.Filename == filename {
			return savedTeam, nil
		}
		return nil, &notFoundError{}
	}
	ds.NewTeamFunc = func(ctx context.Context, newTeam *fleet.Team) (*fleet.Team, error) {
		newTeam.ID = team.ID
		savedTeam = newTeam
		enrolledTeamSecrets = newTeam.Secrets
		return newTeam, nil
	}
	ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
		savedTeam = team
		return team, nil
	}
	ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
		return nil
	}
	ds.BatchSetInHouseAppsInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
		return nil
	}
	ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
		return nil, nil
	}
	ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
		return nil, 0, nil, nil
	}

	ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
		return nil
	}

	vppToken := &fleet.VPPTokenDB{
		Location:  "Foobar",
		RenewDate: time.Now().Add(24 * 365 * time.Hour),
	}
	ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
		return []*fleet.VPPTokenDB{vppToken}, nil
	}

	ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
		return []*fleet.ABMToken{}, nil
	}
	ds.GetABMTokenCountFunc = func(ctx context.Context) (int, error) {
		return 0, nil
	}
	ds.DeleteSetupExperienceScriptFunc = func(ctx context.Context, teamID *uint) error {
		return nil
	}

	ds.TeamsSummaryFunc = func(ctx context.Context) ([]*fleet.TeamSummary, error) {
		var teamsSummary []*fleet.TeamSummary
		if savedTeam != nil {
			teamsSummary = append(teamsSummary, &fleet.TeamSummary{
				ID:          savedTeam.ID,
				Name:        savedTeam.Name,
				Description: savedTeam.Description,
			})
		}
		return teamsSummary, nil
	}

	ds.GetVPPTokenByTeamIDFunc = func(ctx context.Context, teamID *uint) (*fleet.VPPTokenDB, error) {
		if teamID != nil && *teamID == savedTeam.ID {
			return vppToken, nil
		}
		return nil, &notFoundError{}
	}

	ds.UpdateVPPTokenTeamsFunc = func(ctx context.Context, id uint, teams []uint) (*fleet.VPPTokenDB, error) {
		return vppToken, nil
	}
	ds.GetSoftwareCategoryIDsFunc = func(ctx context.Context, names []string) ([]uint, error) {
		return []uint{}, nil
	}
	ds.BatchApplyCertificateAuthoritiesFunc = func(ctx context.Context, ops fleet.CertificateAuthoritiesBatchOperations) error {
		return nil
	}

	ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, options fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
		return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
	}

	ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
		return nil, nil
	}

	createFakeITunesAndVPPServices(t)

	globalFile, err := os.CreateTemp(t.TempDir(), "*.yml")
	require.NoError(t, err)

	t.Setenv("FLEET_SERVER_URL", fleetServerURL)
	t.Setenv("ORG_NAME", orgName)
	t.Setenv("TEST_TEAM_NAME", teamName)
	t.Setenv("TEST_SECRET", secret)

	_, err = globalFile.WriteString(
		`
controls:
queries:
policies:
agent_options:
org_settings:
  server_settings:
    server_url: $FLEET_SERVER_URL
  org_info:
    contact_url: https://example.com/contact
    org_logo_url: ""
    org_logo_url_light_background: ""
    org_name: ${ORG_NAME}
  mdm:
    volume_purchasing_program:
    - location: Foobar
      teams:
      - "${TEST_TEAM_NAME}"
  secrets: [{"secret":"globalSecret"}]
software:
`,
	)
	require.NoError(t, err)

	teamFile, err := os.CreateTemp(t.TempDir(), "*.yml")
	require.NoError(t, err)

	_, err = teamFile.WriteString(
		`
controls:
queries:
policies:
agent_options:
name: ${TEST_TEAM_NAME}
team_settings:
  secrets: [{"secret":"${TEST_SECRET}"}]
software:
  app_store_apps:
    - app_store_id: '1'
`,
	)
	require.NoError(t, err)

	teamFileDupSecret, err := os.CreateTemp(t.TempDir(), "*.yml")
	require.NoError(t, err)
	_, err = teamFileDupSecret.WriteString(
		`
controls:
queries:
policies:
agent_options:
name: ${TEST_TEAM_NAME}
team_settings:
  secrets: [{"secret":"${TEST_SECRET}"},{"secret":"globalSecret"}]
software:
`,
	)
	require.NoError(t, err)

	// Files out of order
	_, err = RunAppNoChecks([]string{"gitops", "-f", teamFile.Name(), "-f", globalFile.Name(), "--dry-run"})
	require.NoError(t, err)

	// No global file, only team file
	_, err = RunAppNoChecks([]string{"gitops", "-f", teamFile.Name(), "--dry-run"})
	require.NoError(t, err)

	// Global file specified multiple times
	_, err = RunAppNoChecks([]string{"gitops", "-f", globalFile.Name(), "-f", teamFile.Name(), "-f", globalFile.Name(), "--dry-run"})
	require.Error(t, err)
	fmt.Printf("err.Error(): %v\n", err.Error())
	assert.Contains(t, err.Error(), "only one global config file may be provided")

	// Duplicate secret
	_, err = RunAppNoChecks([]string{"gitops", "-f", globalFile.Name(), "-f", teamFileDupSecret.Name(), "--dry-run"})
	require.Error(t, err)
	assert.ErrorContains(t, err, "duplicate enroll secret found")

	ds.GetVPPTokenByTeamIDFuncInvoked = false

	// Dry run
	_ = RunAppForTest(t, []string{"gitops", "-f", globalFile.Name(), "-f", teamFile.Name(), "--dry-run"})
	assert.Equal(t, fleet.AppConfig{}, *savedAppConfig, "AppConfig should be empty")

	// Dry run should not attempt to get the VPP token when applying VPP apps (it may not exist).
	require.False(t, ds.GetVPPTokenByTeamIDFuncInvoked)
	ds.ListTeamsFuncInvoked = false

	// Dry run, deleting other teams
	savedAppConfig = &fleet.AppConfig{}
	_ = RunAppForTest(t, []string{"gitops", "-f", globalFile.Name(), "-f", teamFile.Name(), "--dry-run", "--delete-other-teams"})
	assert.Equal(t, fleet.AppConfig{}, *savedAppConfig, "AppConfig should be empty")
	assert.True(t, ds.ListTeamsFuncInvoked)

	// Real run
	_ = RunAppForTest(t, []string{"gitops", "-f", globalFile.Name(), "-f", teamFile.Name()})
	assert.Equal(t, orgName, savedAppConfig.OrgInfo.OrgName)
	assert.Equal(t, fleetServerURL, savedAppConfig.ServerSettings.ServerURL)
	assert.Len(t, enrolledSecrets, 1)
	require.NotNil(t, savedTeam)
	assert.Equal(t, teamName, savedTeam.Name)
	require.Len(t, enrolledTeamSecrets, 1)
	assert.Equal(t, secret, enrolledTeamSecrets[0].Secret)

	// Dry run again (after team was created by real run)
	ds.GetVPPTokenByTeamIDFuncInvoked = false
	ds.GetSoftwareCategoryIDsFuncInvoked = false
	_ = RunAppForTest(t, []string{"gitops", "-f", globalFile.Name(), "-f", teamFile.Name(), "--dry-run"})
	// Dry run should attempt to get the VPP token when applying VPP apps (it may not exist), so we want to error to the user.
	require.True(t, ds.GetVPPTokenByTeamIDFuncInvoked)
	require.True(t, ds.GetSoftwareCategoryIDsFuncInvoked)

	// Now, set  up a team to delete
	teamToDeleteID := uint(999)
	teamToDelete := &fleet.Team{
		ID:        teamToDeleteID,
		CreatedAt: time.Now(),
		Name:      "Team to delete",
	}
	ds.ListTeamsFuncInvoked = false
	ds.ListTeamsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.ListOptions) ([]*fleet.Team, error) {
		return []*fleet.Team{teamToDelete, team}, nil
	}
	ds.TeamLiteFunc = func(ctx context.Context, tid uint) (*fleet.TeamLite, error) {
		switch tid {
		case team.ID:
			return team.ToTeamLite(), nil
		case teamToDeleteID:
			return teamToDelete.ToTeamLite(), nil
		}
		assert.Fail(t, fmt.Sprintf("unexpected team ID %d", tid))
		return teamToDelete.ToTeamLite(), nil
	}
	ds.DeleteTeamFunc = func(ctx context.Context, tid uint) error {
		assert.Equal(t, teamToDeleteID, tid)
		return nil
	}
	ds.ListHostsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.HostListOptions) ([]*fleet.Host, error) {
		return nil, nil
	}

	// Real run, deleting other teams
	_ = RunAppForTest(t, []string{"gitops", "-f", globalFile.Name(), "-f", teamFile.Name(), "--delete-other-teams"})
	assert.True(t, ds.ListTeamsFuncInvoked)
	assert.True(t, ds.DeleteTeamFuncInvoked)
}

func TestGitOpsBasicGlobalAndNoTeam(t *testing.T) {
	// Cannot run t.Parallel() because runServerWithMockedDS sets the FLEET_SERVER_ADDRESS
	// environment variable.

	license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
	_, ds := testing_utils.RunServerWithMockedDS(
		t, &service.TestServerOpts{
			License:       license,
			KeyValueStore: testing_utils.NewMemKeyValueStore(),
		},
	)
	// Mock appConfig
	savedAppConfig := &fleet.AppConfig{}
	ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
		return &fleet.AppConfig{}, nil
	}
	ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
		savedAppConfig = config
		return nil
	}
	ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
		return nil
	}
	ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
		return nil
	}
	ds.DeleteIconsAssociatedWithTitlesWithoutInstallersFunc = func(ctx context.Context, teamID uint) error {
		return nil
	}

	const (
		fleetServerURL = "https://fleet.example.com"
		orgName        = "GitOps Test"
		secret         = "TestSecret"
	)
	var enrolledSecrets []*fleet.EnrollSecret
	var enrolledTeamSecrets []*fleet.EnrollSecret
	var savedTeam *fleet.Team
	team := &fleet.Team{
		ID:        1,
		CreatedAt: time.Now(),
		Name:      teamName,
	}

	ds.IsEnrollSecretAvailableFunc = func(ctx context.Context, secret string, isNew bool, teamID *uint) (bool, error) {
		return true, nil
	}
	ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
		if teamID == nil {
			enrolledSecrets = secrets
		} else {
			enrolledTeamSecrets = secrets
		}
		return nil
	}
	ds.BatchSetMDMProfilesFunc = func(
		ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
		macDecls []*fleet.MDMAppleDeclaration, androidProfiles []*fleet.MDMAndroidConfigProfile, vars []fleet.MDMProfileIdentifierFleetVariables,
	) (updates fleet.MDMProfilesUpdates, err error) {
		assert.Empty(t, macProfiles)
		assert.Empty(t, winProfiles)
		assert.Empty(t, macDecls)
		assert.Empty(t, androidProfiles)
		return fleet.MDMProfilesUpdates{}, nil
	}
	ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) ([]fleet.ScriptResponse, error) {
		assert.Empty(t, scripts)
		return []fleet.ScriptResponse{}, nil
	}
	ds.BulkSetPendingMDMHostProfilesFunc = func(
		ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
	) (updates fleet.MDMProfilesUpdates, err error) {
		assert.Empty(t, profileUUIDs)
		return fleet.MDMProfilesUpdates{}, nil
	}
	ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error {
		return nil
	}
	ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
		require.ElementsMatch(t, labels, []string{fleet.BuiltinLabelMacOS14Plus})
		return map[string]uint{fleet.BuiltinLabelMacOS14Plus: 1}, nil
	}
	ds.ListGlobalPoliciesFunc = func(ctx context.Context, opts fleet.ListOptions) ([]*fleet.Policy, error) { return nil, nil }
	ds.ListTeamPoliciesFunc = func(
		ctx context.Context, teamID uint, opts fleet.ListOptions, iopts fleet.ListOptions,
	) (teamPolicies []*fleet.Policy, inheritedPolicies []*fleet.Policy, err error) {
		return nil, nil, nil
	}
	ds.ListTeamsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.ListOptions) ([]*fleet.Team, error) {
		return nil, nil
	}
	ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
		return nil, 0, nil, nil
	}
	testing_utils.AddLabelMocks(ds)

	ds.NewActivityFunc = func(
		ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
	) error {
		return nil
	}
	ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
		job.ID = 1
		return job, nil
	}
	// Track default team config for team 0
	defaultTeamConfig := &fleet.TeamConfig{}

	ds.TeamLiteFunc = func(ctx context.Context, tid uint) (*fleet.TeamLite, error) {
		if tid == 0 {
			// Return a mock team 0 with the default config
			return &fleet.TeamLite{
				ID:     0,
				Name:   fleet.ReservedNameNoTeam,
				Config: defaultTeamConfig.ToLite(),
			}, nil
		}
		if tid == team.ID {
			return savedTeam.ToTeamLite(), nil
		}
		return nil, nil
	}

	ds.DefaultTeamConfigFunc = func(ctx context.Context) (*fleet.TeamConfig, error) {
		return defaultTeamConfig, nil
	}

	ds.SaveDefaultTeamConfigFunc = func(ctx context.Context, config *fleet.TeamConfig) error {
		defaultTeamConfig = config
		return nil
	}
	ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
		if name == teamName && savedTeam != nil {
			return savedTeam, nil
		}
		return nil, &notFoundError{}
	}
	ds.TeamByFilenameFunc = func(ctx context.Context, filename string) (*fleet.Team, error) {
		if savedTeam != nil && *savedTeam.Filename == filename {
			return savedTeam, nil
		}
		return nil, &notFoundError{}
	}
	ds.NewTeamFunc = func(ctx context.Context, newTeam *fleet.Team) (*fleet.Team, error) {
		newTeam.ID = team.ID
		savedTeam = newTeam
		enrolledTeamSecrets = newTeam.Secrets
		return newTeam, nil
	}
	ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
		savedTeam = team
		return team, nil
	}
	ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
		return nil
	}
	ds.BatchSetInHouseAppsInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
		return nil
	}
	ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
		return nil, nil
	}
	ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
		return nil, 0, nil, nil
	}

	ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
		return nil
	}

	ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
		return []*fleet.VPPTokenDB{}, nil
	}

	ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
		return []*fleet.ABMToken{}, nil
	}
	ds.DeleteSetupExperienceScriptFunc = func(ctx context.Context, teamID *uint) error {
		return nil
	}
	ds.BatchApplyCertificateAuthoritiesFunc = func(ctx context.Context, ops fleet.CertificateAuthoritiesBatchOperations) error {
		return nil
	}

	ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, options fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
		return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
	}

	ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
		return nil, nil
	}

	globalFileBasic := createGlobalFileBasic(t, fleetServerURL, orgName)

	teamFileBasic := createTeamFileBasic(t, secret)

	// We cannot use os.CreateTemp because the filename must be exactly "no-team.yml"
	noTeamFilePath := filepath.Join(t.TempDir(), "no-team.yml")
	noTeamFileBasic, err := os.Create(noTeamFilePath)
	require.NoError(t, err)
	_, err = noTeamFileBasic.WriteString(`
controls:
policies:
name: No team
software:
`)
	require.NoError(t, err)

	t.Run("global defines software -- should fail", func(t *testing.T) {
		globalFileWithSoftware, err := os.CreateTemp(t.TempDir(), "*.yml")
		require.NoError(t, err)
		_, err = globalFileWithSoftware.WriteString(fmt.Sprintf(
			`
controls:
queries:
policies:
agent_options:
org_settings:
  server_settings:
    server_url: %s
  org_info:
    contact_url: https://example.com/contact
    org_logo_url: ""
    org_logo_url_light_background: ""
    org_name: %s
  secrets: [{"secret":"globalSecret"}]
software:
  packages:
    - url: https://example.com
`, fleetServerURL, orgName),
		)
		require.NoError(t, err)

		// Dry run, global defines software, should fail.
		_, err = RunAppNoChecks([]string{
			"gitops", "-f", globalFileWithSoftware.Name(), "-f", teamFileBasic.Name(), "-f",
			noTeamFileBasic.Name(),
			"--dry-run",
		})
		require.Error(t, err)
		assert.ErrorContains(t, err, "'software' cannot be set on global file")
		// Real run, global defines software, should fail.
		_, err = RunAppNoChecks([]string{
			"gitops", "-f", globalFileWithSoftware.Name(), "-f", teamFileBasic.Name(), "-f",
			noTeamFileBasic.Name(),
		})
		require.Error(t, err)
		assert.ErrorContains(t, err, "'software' cannot be set on global file")
	})

	t.Run("both global and no-team.yml define controls -- should fail", func(t *testing.T) {
		globalFileWithControls := createGlobalFileWithControls(t, fleetServerURL, orgName)

		noTeamFilePathWithControls := filepath.Join(t.TempDir(), "no-team.yml")
		noTeamFileWithControls, err := os.Create(noTeamFilePathWithControls)
		require.NoError(t, err)
		_, err = noTeamFileWithControls.WriteString(`
controls:
  ipados_updates:
    deadline: "2023-03-03"
    minimum_version: "18.0"
policies:
name: No team
software:
`)
		require.NoError(t, err)

		// Dry run, both global and no-team.yml define controls.
		_, err = RunAppNoChecks([]string{
			"gitops", "-f", globalFileWithControls.Name(), "-f", teamFileBasic.Name(), "-f",
			noTeamFileWithControls.Name(), "--dry-run",
		})
		require.Error(t, err)
		assert.True(t, strings.Contains(err.Error(), "'controls' cannot be set on both global config and on no-team.yml"))
		// Real run, both global and no-team.yml define controls.
		_, err = RunAppNoChecks([]string{
			"gitops", "-f", globalFileWithControls.Name(), "-f", teamFileBasic.Name(), "-f",
			noTeamFileWithControls.Name(),
		})
		require.Error(t, err)
		assert.True(t, strings.Contains(err.Error(), "'controls' cannot be set on both global config and on no-team.yml"))
	})

	t.Run("no-team.yml defines policy with calendar events enabled -- should fail", func(t *testing.T) {
		globalFileWithControls := createGlobalFileWithControls(t, fleetServerURL, orgName)

		noTeamFilePathPoliciesCalendarPath := filepath.Join(t.TempDir(), "no-team.yml")
		noTeamFilePathPoliciesCalendar, err := os.Create(noTeamFilePathPoliciesCalendarPath)
		require.NoError(t, err)
		_, err = noTeamFilePathPoliciesCalendar.WriteString(`
controls:
policies:
  - name: Foobar
    query: SELECT 1 FROM osquery_info WHERE start_time < 0;
    calendar_events_enabled: true
name: No team
software:
`)
		require.NoError(t, err)

		// Dry run, both global and no-team.yml defines policy with calendar events enabled.
		_, err = RunAppNoChecks([]string{
			"gitops", "-f", globalFileWithControls.Name(), "-f", teamFileBasic.Name(), "-f",
			noTeamFilePathPoliciesCalendar.Name(), "--dry-run",
		})
		require.Error(t, err)
		assert.True(t, strings.Contains(err.Error(), "calendar events are not supported on \"No team\" policies: \"Foobar\""), err.Error())
		// Real run, both global and no-team.yml define controls.
		_, err = RunAppNoChecks([]string{
			"gitops", "-f", globalFileWithControls.Name(), "-f", teamFileBasic.Name(), "-f",
			noTeamFilePathPoliciesCalendar.Name(),
		})
		require.Error(t, err)
		assert.True(t, strings.Contains(err.Error(), "calendar events are not supported on \"No team\" policies: \"Foobar\""), err.Error())
	})

	t.Run("global and no-team.yml DO NOT define controls -- should fail", func(t *testing.T) {
		globalFileWithoutControlsAndSoftwareKeys := createGlobalFileWithoutControlsAndSoftwareKeys(t, fleetServerURL, orgName)

		noTeamFilePathWithoutControls := filepath.Join(t.TempDir(), "no-team.yml")
		noTeamFileWithoutControls, err := os.Create(noTeamFilePathWithoutControls)
		require.NoError(t, err)
		_, err = noTeamFileWithoutControls.WriteString(`
policies:
name: No team
software:
`)
		require.NoError(t, err)

		// Dry run, controls should be defined somewhere, either in no-team.yml or global.
		_, err = RunAppNoChecks([]string{
			"gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), "-f", teamFileBasic.Name(), "-f",
			noTeamFileWithoutControls.Name(), "--dry-run",
		})
		require.Error(t, err)
		assert.True(t, strings.Contains(err.Error(), "'controls' must be set on global config or no-team.yml"))
		// Real run
		_, err = RunAppNoChecks([]string{
			"gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), "-f", teamFileBasic.Name(), "-f",
			noTeamFileWithoutControls.Name(),
		})
		require.Error(t, err)
		assert.True(t, strings.Contains(err.Error(), "'controls' must be set on global config or no-team.yml"))
	})

	t.Run("controls only defined in no-team.yml", func(t *testing.T) {
		savedAppConfig = &fleet.AppConfig{}

		globalFileWithoutControlsAndSoftwareKeys := createGlobalFileWithoutControlsAndSoftwareKeys(t, fleetServerURL, orgName)

		// Dry run, global file without controls and software keys.
		_ = RunAppForTest(t,
			[]string{
				"gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), "-f", teamFileBasic.Name(), "-f",
				noTeamFileBasic.Name(),
				"--dry-run",
			})
		assert.Equal(t, fleet.AppConfig{}, *savedAppConfig, "AppConfig should be empty")

		// Real run, global file without controls and software keys.
		_ = RunAppForTest(t,
			[]string{
				"gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), "-f", teamFileBasic.Name(), "-f",
				noTeamFileBasic.Name(),
			})
		assert.Equal(t, orgName, savedAppConfig.OrgInfo.OrgName)
		assert.Equal(t, fleetServerURL, savedAppConfig.ServerSettings.ServerURL)
		assert.Len(t, enrolledSecrets, 1)
		require.NotNil(t, savedTeam)
		assert.Equal(t, teamName, savedTeam.Name)
		require.Len(t, enrolledTeamSecrets, 1)
		assert.Equal(t, secret, enrolledTeamSecrets[0].Secret)
	})

	t.Run("basic global and no-team.yml", func(t *testing.T) {
		savedAppConfig = &fleet.AppConfig{}
		// Dry run
		_ = RunAppForTest(t,
			[]string{"gitops", "-f", globalFileBasic.Name(), "-f", teamFileBasic.Name(), "-f", noTeamFileBasic.Name(), "--dry-run"})
		assert.Equal(t, fleet.AppConfig{}, *savedAppConfig, "AppConfig should be empty")
		// Real run
		_ = RunAppForTest(t, []string{"gitops", "-f", globalFileBasic.Name(), "-f", teamFileBasic.Name(), "-f", noTeamFileBasic.Name()})
		assert.Equal(t, orgName, savedAppConfig.OrgInfo.OrgName)
		assert.Equal(t, fleetServerURL, savedAppConfig.ServerSettings.ServerURL)
		assert.Len(t, enrolledSecrets, 1)
		require.NotNil(t, savedTeam)
		assert.Equal(t, teamName, savedTeam.Name)
		require.Len(t, enrolledTeamSecrets, 1)
		assert.Equal(t, secret, enrolledTeamSecrets[0].Secret)
	})
}

func createTeamFileBasic(t *testing.T, secret string) *os.File {
	teamFileBasic, err := os.CreateTemp(t.TempDir(), "*.yml")
	require.NoError(t, err)
	_, err = teamFileBasic.WriteString(fmt.Sprintf(`
controls:
queries:
policies:
agent_options:
name: %s
team_settings:
  secrets: [{"secret":"%s"}]
software:
`, teamName, secret),
	)
	require.NoError(t, err)
	return teamFileBasic
}

func createGlobalFileBasic(t *testing.T, fleetServerURL string, orgName string) *os.File {
	globalFileBasic, err := os.CreateTemp(t.TempDir(), "*.yml")
	require.NoError(t, err)
	_, err = globalFileBasic.WriteString(fmt.Sprintf(
		`
controls:
queries:
policies:
agent_options:
org_settings:
  server_settings:
    server_url: %s
  org_info:
    contact_url: https://example.com/contact
    org_logo_url: ""
    org_logo_url_light_background: ""
    org_name: %s
  secrets: [{"secret":"globalSecret"}]
software:
`, fleetServerURL, orgName),
	)
	require.NoError(t, err)
	return globalFileBasic
}

func createGlobalFileWithoutControlsAndSoftwareKeys(t *testing.T, fleetServerURL string, orgName string) *os.File {
	globalFileWithoutControlsAndSoftwareKeys, err := os.CreateTemp(t.TempDir(), "*.yml")
	require.NoError(t, err)
	_, err = globalFileWithoutControlsAndSoftwareKeys.WriteString(fmt.Sprintf(
		`
queries:
policies:
agent_options:
org_settings:
  server_settings:
    server_url: %s
  org_info:
    contact_url: https://example.com/contact
    org_logo_url: ""
    org_logo_url_light_background: ""
    org_name: %s
  secrets: [{"secret":"globalSecret"}]
`, fleetServerURL, orgName),
	)
	require.NoError(t, err)
	return globalFileWithoutControlsAndSoftwareKeys
}

func createGlobalFileWithControls(t *testing.T, fleetServerURL string, orgName string) *os.File {
	globalFileWithControls, err := os.CreateTemp(t.TempDir(), "*.yml")
	require.NoError(t, err)
	_, err = globalFileWithControls.WriteString(fmt.Sprintf(
		`
controls:
  ios_updates:
    deadline: "2022-02-02"
    minimum_version: "17.6"
queries:
policies:
agent_options:
org_settings:
  server_settings:
    server_url: %s
  org_info:
    contact_url: https://example.com/contact
    org_logo_url: ""
    org_logo_url_light_background: ""
    org_name: %s
  secrets: [{"secret":"globalSecret"}]
software:
`, fleetServerURL, orgName),
	)
	require.NoError(t, err)
	return globalFileWithControls
}

func TestGitOpsFullGlobalAndTeam(t *testing.T) {
	// Cannot run t.Parallel() because it sets environment variables
	// mdm test configuration must be set so that activating windows MDM works.
	ds, savedAppConfigPtr, savedTeams := testing_utils.SetupFullGitOpsPremiumServer(t)
	testing_utils.StartSoftwareInstallerServer(t)

	var enrolledSecrets []*fleet.EnrollSecret
	var enrolledTeamSecrets []*fleet.EnrollSecret
	var appliedPolicySpecs []*fleet.PolicySpec
	var appliedQueries []*fleet.Query

	ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
		if teamID == nil {
			enrolledSecrets = secrets
		} else {
			enrolledTeamSecrets = secrets
		}
		return nil
	}
	ds.ApplyPolicySpecsFunc = func(ctx context.Context, authorID uint, specs []*fleet.PolicySpec) error {
		appliedPolicySpecs = specs
		return nil
	}
	ds.ApplyQueriesFunc = func(
		ctx context.Context, authorID uint, queries []*fleet.Query, queriesToDiscardResults map[uint]struct{},
	) error {
		appliedQueries = queries
		return nil
	}
	ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
		team.ID = 1
		enrolledTeamSecrets = team.Secrets
		savedTeams[team.Name] = &team
		return team, nil
	}

	ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
		return nil
	}

	ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
		return []*fleet.VPPTokenDB{}, nil
	}

	ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
		return []*fleet.ABMToken{}, nil
	}
	ds.GetABMTokenCountFunc = func(ctx context.Context) (int, error) {
		return 0, nil
	}
	ds.GetTeamsWithInstallerByHashFunc = func(ctx context.Context, sha256, url string) (map[uint][]*fleet.ExistingSoftwareInstaller, error) {
		return map[uint][]*fleet.ExistingSoftwareInstaller{}, nil
	}
	ds.GetSoftwareCategoryIDsFunc = func(ctx context.Context, names []string) ([]uint, error) {
		return []uint{}, nil
	}
	ds.DeleteIconsAssociatedWithTitlesWithoutInstallersFunc = func(ctx context.Context, teamID uint) error {
		return nil
	}

	apnsCert, apnsKey, err := mysql.GenerateTestCertBytes(mdmtesting.NewTestMDMAppleCertTemplate())
	require.NoError(t, err)
	crt, key, err := apple_mdm.NewSCEPCACertKey()
	require.NoError(t, err)
	scepCert := tokenpki.PEMCertificate(crt.Raw)
	scepKey := tokenpki.PEMRSAPrivateKey(key)

	ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
		_ sqlx.QueryerContext,
	) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
		return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
			fleet.MDMAssetCACert:   {Value: scepCert},
			fleet.MDMAssetCAKey:    {Value: scepKey},
			fleet.MDMAssetAPNSKey:  {Value: apnsKey},
			fleet.MDMAssetAPNSCert: {Value: apnsCert},
		}, nil
	}

	ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
		return nil
	}
	ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
		return nil
	}

	ds.LabelsByNameFunc = func(ctx context.Context, names []string) (map[string]*fleet.Label, error) {
		return map[string]*fleet.Label{
			"a": {
				ID:   1,
				Name: "a",
			},
			"b": {
				ID:   2,
				Name: "b",
			},
		}, nil
	}

	ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, options fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
		return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
	}

	ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
		return nil, nil
	}

	globalFile := "./testdata/gitops/global_config_no_paths.yml"
	teamFile := "./testdata/gitops/team_config_no_paths.yml"

	t.Setenv("ENABLE_DISK_ENCRYPTION", "true")
	t.Setenv("WINDOWS_REQUIRE_BITLOCKER_PIN", "true")

	// Dry run
	_ = RunAppForTest(t, []string{"gitops", "-f", globalFile, "-f", teamFile, "--dry-run", "--delete-other-teams"})
	assert.False(t, ds.SaveAppConfigFuncInvoked)
	assert.Len(t, enrolledSecrets, 0)
	assert.Len(t, enrolledTeamSecrets, 0)
	assert.Len(t, appliedPolicySpecs, 0)
	assert.Len(t, appliedQueries, 0)

	// Real run
	_ = RunAppForTest(t, []string{"gitops", "-f", globalFile, "-f", teamFile, "--delete-other-teams"})
	assert.Equal(t, orgName, (*savedAppConfigPtr).OrgInfo.OrgName)
	assert.Equal(t, fleetServerURL, (*savedAppConfigPtr).ServerSettings.ServerURL)
	assert.Len(t, enrolledSecrets, 2)
	require.NotNil(t, *savedTeams[teamName])
	assert.Equal(t, teamName, (*savedTeams[teamName]).Name)
	require.Len(t, enrolledTeamSecrets, 2)

	t.Run("no-team.yml using relative paths", func(t *testing.T) {
		globalFileBasic := createGlobalFileBasic(t, fleetServerURL, orgName)
		teamFileBasic := createTeamFileBasic(t, teamName)

		noTeamDir := t.TempDir()
		noTeamFile, err := os.Create(filepath.Join(noTeamDir, "no-team.yml"))
		require.NoError(t, err)
		_, err = noTeamFile.WriteString(`
controls:
  scripts:
    - path: ./script.sh
  windows_enabled_and_configured: true
  android_enabled_and_configured: true
  macos_settings:
    custom_settings:
    - path: ./config.json
  windows_settings:
    custom_settings:
    - path: ./config2.xml
  android_settings:
    custom_settings:
    - path: ./config3.json
policies:
name: No team
software:
`)
		require.NoError(t, err)

		ddmFile, err := os.Create(filepath.Join(noTeamDir, "config.json"))
		require.NoError(t, err)
		_, err = ddmFile.WriteString(`
{
    "Type": "com.apple.configuration.passcode.settings",
    "Identifier": "com.fleetdm.config.passcode.settings",
    "Payload": {
        "RequireAlphanumericPasscode": true
    }
}
		`)
		require.NoError(t, err)

		cspFile, err := os.Create(filepath.Join(noTeamDir, "config2.xml"))
		require.NoError(t, err)
		_, err = cspFile.WriteString(`<Replace>bozo</Replace>`)
		require.NoError(t, err)

		androidFile, err := os.Create(filepath.Join(noTeamDir, "config3.json"))
		require.NoError(t, err)
		_, err = androidFile.WriteString(`{"name":"Android profile"}`)
		require.NoError(t, err)

		scriptFile, err := os.Create(filepath.Join(noTeamDir, "script.sh"))
		require.NoError(t, err)
		_, err = scriptFile.WriteString(`echo "Hello, world!"`)
		require.NoError(t, err)

		// Validate that global config is required when running noTeam
		_, err = RunAppNoChecks(
			[]string{"gitops", "-f", noTeamFile.Name(), "--dry-run"})
		assert.Error(t, err)

		// Dry run
		ds.SaveAppConfigFuncInvoked = false
		ds.BatchSetScriptsFuncInvoked = false
		_ = RunAppForTest(t,
			[]string{"gitops", "-f", globalFileBasic.Name(), "-f", teamFileBasic.Name(), "-f", noTeamFile.Name(), "--dry-run"})
		assert.False(t, ds.SaveAppConfigFuncInvoked)
		assert.False(t, ds.BatchSetScriptsFuncInvoked)

		// Real run
		_ = RunAppForTest(t, []string{"gitops", "-f", globalFileBasic.Name(), "-f", teamFileBasic.Name(), "-f", noTeamFile.Name()})
		assert.Equal(t, orgName, (*savedAppConfigPtr).OrgInfo.OrgName)
		assert.Equal(t, fleetServerURL, (*savedAppConfigPtr).ServerSettings.ServerURL)
		require.Len(t, (*savedAppConfigPtr).MDM.MacOSSettings.CustomSettings, 1)
		assert.Equal(t, filepath.Base(ddmFile.Name()), filepath.Base((*savedAppConfigPtr).MDM.MacOSSettings.CustomSettings[0].Path))
		require.Len(t, (*savedAppConfigPtr).MDM.WindowsSettings.CustomSettings.Value, 1)
		assert.Equal(t, filepath.Base(cspFile.Name()), filepath.Base((*savedAppConfigPtr).MDM.WindowsSettings.CustomSettings.Value[0].Path))
		require.Len(t, (*savedAppConfigPtr).MDM.AndroidSettings.CustomSettings.Value, 1)
		assert.Equal(t, filepath.Base(androidFile.Name()), filepath.Base((*savedAppConfigPtr).MDM.AndroidSettings.CustomSettings.Value[0].Path))
		assert.True(t, ds.BatchSetScriptsFuncInvoked)

		// Get applied policies for the team
		teamAppliedPoliceSpecs := make([]*fleet.PolicySpec, 0)
		for _, appliedPolicySpec := range appliedPolicySpecs {
			if appliedPolicySpec.Team == teamName {
				teamAppliedPoliceSpecs = append(teamAppliedPoliceSpecs, appliedPolicySpec)
			}
		}
		assert.Len(t, teamAppliedPoliceSpecs, 5)
		assert.Len(t, teamAppliedPoliceSpecs[0].LabelsIncludeAny, 0)
		assert.Len(t, teamAppliedPoliceSpecs[0].LabelsExcludeAny, 1)
		assert.Equal(t, teamAppliedPoliceSpecs[0].LabelsExcludeAny[0], "a")
		assert.Len(t, teamAppliedPoliceSpecs[1].LabelsIncludeAny, 1)
		assert.Len(t, teamAppliedPoliceSpecs[1].LabelsExcludeAny, 0)
		assert.Equal(t, teamAppliedPoliceSpecs[1].LabelsIncludeAny[0], "b")
	})
}

func TestGitOpsCustomSettings(t *testing.T) {
	cases := []struct {
		file    string
		wantErr string
	}{
		{"testdata/gitops/global_macos_windows_custom_settings_valid.yml", ""},
		{"testdata/gitops/global_macos_custom_settings_valid_deprecated.yml", ""},
		{"testdata/gitops/global_windows_custom_settings_invalid_label_mix.yml", "please choose one of `labels_include_any`, `labels_include_all` or `labels_exclude_any`"},
		{"testdata/gitops/global_windows_custom_settings_invalid_label_mix_2.yml", "please choose one of `labels_include_any`, `labels_include_all` or `labels_exclude_any`"},
		{"testdata/gitops/global_windows_custom_settings_unknown_label.yml", `Please create the missing labels, or update your settings to not refer to these labels.`},
		{"testdata/gitops/team_macos_windows_custom_settings_valid.yml", ""},
		{"testdata/gitops/team_macos_custom_settings_valid_deprecated.yml", ""},
		{"testdata/gitops/team_macos_windows_custom_settings_invalid_labels_mix.yml", "please choose one of `labels_include_any`, `labels_include_all` or `labels_exclude_any`"},
		{"testdata/gitops/team_macos_windows_custom_settings_invalid_labels_mix_2.yml", "please choose one of `labels_include_any`, `labels_include_all` or `labels_exclude_any`"},
		{"testdata/gitops/team_macos_windows_custom_settings_unknown_label.yml", `Please create the missing labels, or update your settings to not refer to these labels.`},
	}
	for _, c := range cases {
		t.Run(filepath.Base(c.file), func(t *testing.T) {
			ds, appCfgPtr, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
			(*appCfgPtr).MDM.EnabledAndConfigured = true
			(*appCfgPtr).MDM.WindowsEnabledAndConfigured = true
			ds.GetLabelSpecsFunc = func(ctx context.Context) ([]*fleet.LabelSpec, error) {
				return []*fleet.LabelSpec{
					{
						Name:                "A",
						Description:         "A global label",
						LabelMembershipType: fleet.LabelMembershipTypeManual,
						Hosts:               []string{"host2", "host3"},
					},
					{
						Name:                "B",
						Description:         "Another label",
						LabelMembershipType: fleet.LabelMembershipTypeDynamic,
						Query:               "SELECT 1 from osquery_info",
					},
					{
						Name:                "C",
						Description:         "Another nother label",
						LabelMembershipType: fleet.LabelMembershipTypeDynamic,
						Query:               "SELECT 1 from osquery_info",
					},
				}, nil
			}
			labelToIDs := map[string]uint{
				fleet.BuiltinLabelMacOS14Plus: 1,
				"A":                           2,
				"B":                           3,
				"C":                           4,
			}

			ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
				// for this test, recognize labels A, B and C (as well as the built-in macos 14+ one)
				ret := make(map[string]uint)
				for _, lbl := range labels {
					id, ok := labelToIDs[lbl]
					if ok {
						ret[lbl] = id
					}
				}
				return ret, nil
			}
			ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
				return nil
			}
			ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
				return nil
			}
			ds.DeleteIconsAssociatedWithTitlesWithoutInstallersFunc = func(ctx context.Context, teamID uint) error {
				return nil
			}

			ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, options fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
				return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
			}

			ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
				return nil, nil
			}

			_, err := RunAppNoChecks([]string{"gitops", "-f", c.file})
			if c.wantErr == "" {
				require.NoError(t, err)
			} else {
				require.ErrorContains(t, err, c.wantErr)
			}
		})
	}
}

func TestGitOpsABM(t *testing.T) {
	global := func(mdm string) string {
		return fmt.Sprintf(`
controls:
queries:
policies:
agent_options:
software:
org_settings:
  server_settings:
    server_url: "https://foo.example.com"
  org_info:
    org_name: GitOps Test
  secrets:
    - secret: "global"
  mdm:
    %s
 `, mdm)
	}

	team := func(name string) string {
		return fmt.Sprintf(`
name: %s
team_settings:
  secrets:
    - secret: "%s-secret"
agent_options:
controls:
policies:
queries:
software:
`, name, name)
	}

	workstations := team("💻 Workstations")
	iosTeam := team("📱🏢 Company-owned iPhones")
	ipadTeam := team("🔳🏢 Company-owned iPads")

	cases := []struct {
		name             string
		cfgs             []string
		dryRunAssertion  func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error)
		realRunAssertion func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error)
		tokens           []*fleet.ABMToken
	}{
		{
			name: "backwards compat",
			cfgs: []string{
				global("apple_bm_default_team: 💻 Workstations"),
				workstations,
			},
			tokens: []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}},
			dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
				assert.NoError(t, err)
				assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
				assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
				assert.Contains(t, out, "[!] gitops dry run succeeded")
			},
			realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
				assert.NoError(t, err)
				assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
				assert.Equal(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam, "💻 Workstations")
				assert.Contains(t, out, "[!] gitops succeeded")
			},
		},
		{
			name: "deprecated config with two tokens in the db fails",
			cfgs: []string{
				global("apple_bm_default_team: 💻 Workstations"),
				workstations,
			},
			tokens: []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}, {OrganizationName: "Second Token LLC"}},
			dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
				t.Logf("got: %s", out)
				require.ErrorContains(t, err, "mdm.apple_bm_default_team has been deprecated")
				assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
				assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
				assert.NotContains(t, out, "[!] gitops dry run succeeded")
			},
			realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
				require.ErrorContains(t, err, "mdm.apple_bm_default_team has been deprecated")
				assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
				assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
				assert.NotContains(t, out, "[!] gitops succeeded")
			},
		},
		{
			name: "new key all valid",
			cfgs: []string{
				global(`
                                  apple_business_manager:
                                    - organization_name: Fleet Device Management Inc.
                                      macos_team: "💻 Workstations"
                                      ios_team: "📱🏢 Company-owned iPhones"
                                      ipados_team: "🔳🏢 Company-owned iPads"`),
				workstations,
				iosTeam,
				ipadTeam,
			},
			dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
				assert.NoError(t, err)
				assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
				assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
				assert.Contains(t, out, "[!] gitops dry run succeeded")
			},
			realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
				assert.NoError(t, err)
				assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
				assert.ElementsMatch(
					t,
					appCfg.MDM.AppleBusinessManager.Value,
					[]fleet.MDMAppleABMAssignmentInfo{
						{
							OrganizationName: "Fleet Device Management Inc.",
							MacOSTeam:        "💻 Workstations",
							IOSTeam:          "📱🏢 Company-owned iPhones",
							IpadOSTeam:       "🔳🏢 Company-owned iPads",
						},
					},
				)
				assert.Contains(t, out, "[!] gitops succeeded")
			},
		},
		{
			name: "new key multiple elements",
			cfgs: []string{
				global(`
                                  apple_business_manager:
                                    - organization_name: Foo Inc.
                                      macos_team: "💻 Workstations"
                                      ios_team: "📱🏢 Company-owned iPhones"
                                      ipados_team: "🔳🏢 Company-owned iPads"
                                    - organization_name: Fleet Device Management Inc.
                                      macos_team: "💻 Workstations"
                                      ios_team: "📱🏢 Company-owned iPhones"
                                      ipados_team: "🔳🏢 Company-owned iPads"`),
				workstations,
				iosTeam,
				ipadTeam,
			},
			dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
				assert.NoError(t, err)
				assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
				assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
				assert.Contains(t, out, "[!] gitops dry run succeeded")
			},
			realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
				assert.NoError(t, err)
				assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
				assert.ElementsMatch(
					t,
					appCfg.MDM.AppleBusinessManager.Value,
					[]fleet.MDMAppleABMAssignmentInfo{
						{
							OrganizationName: "Fleet Device Management Inc.",
							MacOSTeam:        "💻 Workstations",
							IOSTeam:          "📱🏢 Company-owned iPhones",
							IpadOSTeam:       "🔳🏢 Company-owned iPads",
						},
						{
							OrganizationName: "Foo Inc.",
							MacOSTeam:        "💻 Workstations",
							IOSTeam:          "📱🏢 Company-owned iPhones",
							IpadOSTeam:       "🔳🏢 Company-owned iPads",
						},
					},
				)
				assert.Contains(t, out, "[!] gitops succeeded")
			},
		},
		{
			name: "both keys errors",
			cfgs: []string{
				global(`
                                  apple_bm_default_team: "💻 Workstations"
                                  apple_business_manager:
                                    - organization_name: Fleet Device Management Inc.
                                      macos_team: "💻 Workstations"
                                      ios_team: "📱🏢 Company-owned iPhones"
                                      ipados_team: "🔳🏢 Company-owned iPads"`),
				workstations,
				iosTeam,
				ipadTeam,
			},
			dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
				require.ErrorContains(t, err, "mdm.apple_bm_default_team has been deprecated")
				assert.NotContains(t, out, "[!] gitops dry run succeeded")
			},
			realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
				require.ErrorContains(t, err, "mdm.apple_bm_default_team has been deprecated")
				assert.NotContains(t, out, "[!] gitops succeeded")
			},
		},
		{
			name: "using an undefined team errors",
			cfgs: []string{
				global(`
                                  apple_business_manager:
                                    - organization_name: Fleet Device Management Inc.
                                      macos_team: "💻 Workstations"
                                      ios_team: "📱🏢 Company-owned iPhones"
                                      ipados_team: "🔳🏢 Company-owned iPads"`),
				workstations,
			},
			dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
				assert.ErrorContains(t, err, "apple_business_manager team \"📱🏢 Company-owned iPhones\" not found in team configs")
			},
			realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
				assert.ErrorContains(t, err, "apple_business_manager team \"📱🏢 Company-owned iPhones\" not found in team configs")
			},
		},
		{
			name: "no team is supported",
			cfgs: []string{
				global(`
                                  apple_business_manager:
                                    - organization_name: Fleet Device Management Inc.
                                      macos_team: "No team"
                                      ios_team: "No team"
                                      ipados_team: "No team"`),
			},
			dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
				assert.NoError(t, err)
				assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
				assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
				assert.Contains(t, out, "[!] gitops dry run succeeded")
			},
			realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
				assert.NoError(t, err)
				assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
				assert.ElementsMatch(
					t,
					appCfg.MDM.AppleBusinessManager.Value,
					[]fleet.MDMAppleABMAssignmentInfo{
						{
							OrganizationName: "Fleet Device Management Inc.",
							MacOSTeam:        "No team",
							IOSTeam:          "No team",
							IpadOSTeam:       "No team",
						},
					},
				)
				assert.Contains(t, out, "[!] gitops succeeded")
			},
		},
		{
			name: "not provided teams defaults to no team",
			cfgs: []string{
				global(`
                                  apple_business_manager:
                                    - organization_name: Fleet Device Management Inc.
                                      macos_team: "No team"
                                      ios_team: ""`),
			},
			dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
				assert.NoError(t, err)
				assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
				assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
				assert.Contains(t, out, "[!] gitops dry run succeeded")
			},
			realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
				assert.NoError(t, err)
				assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
				assert.ElementsMatch(
					t,
					appCfg.MDM.AppleBusinessManager.Value,
					[]fleet.MDMAppleABMAssignmentInfo{
						{
							OrganizationName: "Fleet Device Management Inc.",
							MacOSTeam:        "No team",
							IOSTeam:          "",
							IpadOSTeam:       "",
						},
					},
				)
				assert.Contains(t, out, "[!] gitops succeeded")
			},
		},
		{
			name: "non existent org name fails",
			cfgs: []string{
				global(`
                                  apple_business_manager:
                                    - organization_name: Does not exist
                                      macos_team: "No team"`),
			},
			tokens: []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}},
			dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
				assert.ErrorContains(t, err, "token with organization name Does not exist doesn't exist")
				assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
				assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
				assert.NotContains(t, out, "[!] gitops dry run succeeded")
			},
			realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
				assert.ErrorContains(t, err, "token with organization name Does not exist doesn't exist")
				assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
				assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
				assert.NotContains(t, out, "[!] gitops dry run succeeded")
			},
		},
	}

	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			ds, savedAppConfigPtr, savedTeams := testing_utils.SetupFullGitOpsPremiumServer(t)

			ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
				if len(tt.tokens) > 0 {
					return tt.tokens, nil
				}
				return []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}, {OrganizationName: "Foo Inc."}}, nil
			}
			ds.GetABMTokenCountFunc = func(ctx context.Context) (int, error) {
				return len(tt.tokens), nil
			}

			ds.TeamsSummaryFunc = func(ctx context.Context) ([]*fleet.TeamSummary, error) {
				var res []*fleet.TeamSummary
				for _, tm := range savedTeams {
					res = append(res, &fleet.TeamSummary{Name: (*tm).Name, ID: (*tm).ID})
				}
				return res, nil
			}

			ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
				return nil
			}
			ds.DeleteIconsAssociatedWithTitlesWithoutInstallersFunc = func(ctx context.Context, teamID uint) error {
				return nil
			}

			ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, options fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
				return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
			}

			ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
				return nil, nil
			}

			args := []string{"gitops"}
			for _, cfg := range tt.cfgs {
				if cfg != "" {
					tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
					require.NoError(t, err)
					_, err = tmpFile.WriteString(cfg)
					require.NoError(t, err)
					args = append(args, "-f", tmpFile.Name())
				}
			}

			// Dry run
			out, err := RunAppNoChecks(append(args, "--dry-run"))
			tt.dryRunAssertion(t, *savedAppConfigPtr, ds, out.String(), err)
			if t.Failed() {
				t.FailNow()
			}

			// Real run
			out, err = RunAppNoChecks(args)
			tt.realRunAssertion(t, *savedAppConfigPtr, ds, out.String(), err)

			// Second real run, now that all the teams are saved
			out, err = RunAppNoChecks(args)
			tt.realRunAssertion(t, *savedAppConfigPtr, ds, out.String(), err)
		})
	}
}

func TestGitOpsWindowsMigration(t *testing.T) {
	cases := []struct {
		file    string
		wantErr string
	}{
		// booleans are Windows MDM enabled and Windows migration enabled
		{"testdata/gitops/global_config_windows_migration_true_true.yml", ""},
		{"testdata/gitops/global_config_windows_migration_false_true.yml", "Windows MDM is not enabled"},
		{"testdata/gitops/global_config_windows_migration_true_false.yml", ""},
		{"testdata/gitops/global_config_windows_migration_false_false.yml", ""},
	}
	for _, c := range cases {
		t.Run(filepath.Base(c.file), func(t *testing.T) {
			testing_utils.SetupFullGitOpsPremiumServer(t)

			_, err := RunAppNoChecks([]string{"gitops", "-f", c.file})
			if c.wantErr == "" {
				require.NoError(t, err)
			} else {
				require.ErrorContains(t, err, c.wantErr)
			}
		})
	}
}

func TestGitOpsGlobalWebhooksDisable(t *testing.T) {
	_, appConfig, _ := testing_utils.SetupFullGitOpsPremiumServer(t)

	webhook := &(*appConfig).WebhookSettings
	webhook.ActivitiesWebhook.Enable = true
	webhook.FailingPoliciesWebhook.Enable = true
	webhook.HostStatusWebhook.Enable = true
	webhook.VulnerabilitiesWebhook.Enable = true

	// Run config with no webooks settings
	_, err := RunAppNoChecks([]string{"gitops", "-f", "testdata/gitops/global_config_windows_migration_true_true.yml"})
	require.NoError(t, err)

	webhook = &(*appConfig).WebhookSettings
	require.False(t, webhook.ActivitiesWebhook.Enable)
	require.False(t, webhook.FailingPoliciesWebhook.Enable)
	require.False(t, webhook.HostStatusWebhook.Enable)
	require.False(t, webhook.VulnerabilitiesWebhook.Enable)
}

func TestGitOpsTeamWebhooks(t *testing.T) {
	teamName := "TestTeamWebhooks"

	ds, _, savedTeams := testing_utils.SetupFullGitOpsPremiumServer(t)

	// Create a new team.
	_, err := ds.NewTeam(context.Background(), &fleet.Team{Name: teamName, Config: fleet.TeamConfig{WebhookSettings: fleet.TeamWebhookSettings{
		FailingPoliciesWebhook: fleet.FailingPoliciesWebhookSettings{Enable: true, DestinationURL: "http://saybye.by"},
		HostStatusWebhook:      &fleet.HostStatusWebhookSettings{Enable: true},
	}}})
	require.NoError(t, err)
	require.NotNil(t, *savedTeams[teamName])

	// Do a GitOps run with no webhook settings.
	t.Setenv("TEST_TEAM_NAME", teamName)
	_, err = RunAppNoChecks([]string{"gitops", "-f", "testdata/gitops/team_config_webhook.yml"})
	require.NoError(t, err)

	team, err := ds.TeamByName(context.Background(), teamName)
	require.NoError(t, err)
	require.NotNil(t, team)
	require.NotNil(t, team.Config.WebhookSettings)

	// Check that the team's failing policy webhook settings are disabled and cleared, since the GitOps
	// config doesn't include them.
	require.False(t, team.Config.WebhookSettings.FailingPoliciesWebhook.Enable)
	require.Equal(t, "", team.Config.WebhookSettings.FailingPoliciesWebhook.DestinationURL)
	// Check that the team's host status webhook settings are enabled and set to the new values.
	require.True(t, team.Config.WebhookSettings.HostStatusWebhook.Enable)
	require.Equal(t, "http://coolwebhook.biz", team.Config.WebhookSettings.HostStatusWebhook.DestinationURL)
}

func TestGitOpsFeatures(t *testing.T) {
	globalFileBasic := createGlobalFileBasic(t, fleetServerURL, orgName)
	ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)

	appConfig := fleet.AppConfig{
		Features: fleet.Features{
			EnableHostUsers:         true,
			EnableSoftwareInventory: true,
			AdditionalQueries:       ptr.RawMessage(json.RawMessage(`{"query_a": "SELECT 1", "query_b": "SELECT 2"}`)),
			DetailQueryOverrides: map[string]*string{
				"detail_query_a": ptr.String("SELECT a"),
				"detail_query_b": nil,
			},
		},
	}

	globalFileUpdatedFeatures, err := os.CreateTemp(t.TempDir(), "*.yml")
	require.NoError(t, err)
	_, err = globalFileUpdatedFeatures.WriteString(fmt.Sprintf(
		`
controls:
queries:
policies:
agent_options:
org_settings:
  features:
    enable_host_users: false
    enable_software_inventory: false
    additional_queries:
      query_a: "SELECT 1"
    detail_query_overrides:
      detail_query_a: "SELECT it_works"
  server_settings:
    server_url: %s
  org_info:
    contact_url: https://example.com/contact
    org_logo_url: ""
    org_logo_url_light_background: ""
    org_name: %s
  secrets: [{"secret":"globalSecret"}]
software:
`, fleetServerURL, orgName),
	)
	require.NoError(t, err)

	ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
		return &appConfig, nil
	}

	ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
		appConfig = *config
		return nil
	}

	// Do a GitOps run with updated feature settings.
	_, err = RunAppNoChecks([]string{"gitops", "-f", globalFileUpdatedFeatures.Name()})
	require.NoError(t, err)
	require.False(t, appConfig.Features.EnableHostUsers)
	require.False(t, appConfig.Features.EnableSoftwareInventory)

	// Parse the additional queries into a map.
	var additionalQueries map[string]string
	err = json.Unmarshal(*appConfig.Features.AdditionalQueries, &additionalQueries)
	require.NoError(t, err)
	require.Equal(t, 1, len(additionalQueries))
	require.Equal(t, "SELECT 1", additionalQueries["query_a"])
	require.Equal(t, 1, len(appConfig.Features.DetailQueryOverrides))
	require.Equal(t, "SELECT it_works", *appConfig.Features.DetailQueryOverrides["detail_query_a"])

	// Do a GitOps run with no feature settings.
	_, err = RunAppNoChecks([]string{"gitops", "-f", globalFileBasic.Name()})
	require.NoError(t, err)

	require.False(t, appConfig.Features.EnableHostUsers)
	require.True(t, appConfig.Features.EnableSoftwareInventory)
	require.Nil(t, appConfig.Features.AdditionalQueries)
	require.Nil(t, appConfig.Features.DetailQueryOverrides)
}

func TestGitOpsSSOSettings(t *testing.T) {
	globalFileBasic := createGlobalFileBasic(t, fleetServerURL, orgName)
	ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)

	appConfig := fleet.AppConfig{
		SSOSettings: &fleet.SSOSettings{
			SSOProviderSettings: fleet.SSOProviderSettings{
				EntityID:  "some-entity-id",
				IssuerURI: "https://example.com/saml",
				Metadata:  "some-metadata",
				IDPName:   "some-idp-name",
			},
			IDPImageURL:           "https://example.com/logo.png",
			EnableSSO:             true,
			EnableSSOIdPLogin:     true,
			EnableJITProvisioning: true,
			EnableJITRoleSync:     true,
		},
	}

	ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
		return &appConfig, nil
	}

	ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
		appConfig = *config
		return nil
	}

	// Do a GitOps run with no sso settings.
	_, err := RunAppNoChecks([]string{"gitops", "-f", globalFileBasic.Name()})
	require.NoError(t, err)

	require.Nil(t, appConfig.SSOSettings)
}

func TestGitOpsSSOServerURL(t *testing.T) {
	tmpDir := t.TempDir()
	globalFile, err := os.CreateTemp(tmpDir, "*.yml")
	require.NoError(t, err)
	_, err = globalFile.WriteString(`
controls:
queries:
policies:
agent_options:
org_settings:
  server_settings:
    server_url: ` + fleetServerURL + `
  org_info:
    org_name: ` + orgName + `
  sso_settings:
    entity_id: "test-entity"
    idp_name: "Test IdP"
    metadata: "<xml>test-metadata</xml>"
    enable_sso: true
    sso_server_url: "https://sso.example.com"
  secrets:
    - secret: test-secret
`)
	require.NoError(t, err)
	globalFile.Close()

	ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)

	appConfig := fleet.AppConfig{}

	ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
		return &appConfig, nil
	}

	ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
		appConfig = *config
		return nil
	}

	// Run GitOps with SSO settings including sso_url
	_, err = RunAppNoChecks([]string{"gitops", "-f", globalFile.Name()})
	require.NoError(t, err)

	require.NotNil(t, appConfig.SSOSettings)
	require.Equal(t, "https://sso.example.com", appConfig.SSOSettings.SSOServerURL)
	require.Equal(t, "test-entity", appConfig.SSOSettings.EntityID)
	require.True(t, appConfig.SSOSettings.EnableSSO)
}

func TestGitOpsSMTPSettings(t *testing.T) {
	globalFileBasic := createGlobalFileBasic(t, fleetServerURL, orgName)
	ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)

	appConfig := fleet.AppConfig{
		SMTPSettings: &fleet.SMTPSettings{
			SMTPEnabled:              true,
			SMTPConfigured:           true,
			SMTPSenderAddress:        "http://example.com",
			SMTPServer:               "server.example.com",
			SMTPPort:                 587,
			SMTPAuthenticationType:   "smoooth",
			SMTPUserName:             "uzer",
			SMTPPassword:             "pazzword",
			SMTPEnableTLS:            true,
			SMTPAuthenticationMethod: "crunchy",
			SMTPDomain:               "smtp.example.com",
			SMTPVerifySSLCerts:       true,
			SMTPEnableStartTLS:       true,
		},
	}

	ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
		return &appConfig, nil
	}

	ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
		appConfig = *config
		return nil
	}

	// Do a GitOps run with no smtp settings.
	_, err := RunAppNoChecks([]string{"gitops", "-f", globalFileBasic.Name()})
	require.NoError(t, err)

	// Currently we do NOT clear the SMTP settings if they are not in the config,
	// because the smtp_settings key is not documented in the GitOps config.
	// TODO - update this test if we change this behavior.
	require.Equal(t, &fleet.SMTPSettings{
		SMTPEnabled:              true,
		SMTPConfigured:           true,
		SMTPSenderAddress:        "http://example.com",
		SMTPServer:               "server.example.com",
		SMTPPort:                 587,
		SMTPAuthenticationType:   "smoooth",
		SMTPUserName:             "uzer",
		SMTPPassword:             "********",
		SMTPEnableTLS:            true,
		SMTPAuthenticationMethod: "crunchy",
		SMTPDomain:               "smtp.example.com",
		SMTPVerifySSLCerts:       true,
		SMTPEnableStartTLS:       true,
	}, appConfig.SMTPSettings)
}

func TestGitOpsMDMAuthSettings(t *testing.T) {
	globalFileBasic := createGlobalFileBasic(t, fleetServerURL, orgName)
	ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)

	appConfig := fleet.AppConfig{
		MDM: fleet.MDM{
			EndUserAuthentication: fleet.MDMEndUserAuthentication{
				SSOProviderSettings: fleet.SSOProviderSettings{
					EntityID:  "some-entity-id",
					IssuerURI: "https://example.com/saml",
					Metadata:  "some-metadata",
					IDPName:   "some-idp-name",
				},
			},
		},
	}

	ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
		return &appConfig, nil
	}

	ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
		appConfig = *config
		return nil
	}

	// Do a GitOps run with no mdm end user auth settings.
	_, err := RunAppNoChecks([]string{"gitops", "-f", globalFileBasic.Name()})
	require.NoError(t, err)

	require.NotNil(t, appConfig.MDM.EndUserAuthentication)
	require.Empty(t, appConfig.MDM.EndUserAuthentication.SSOProviderSettings.EntityID)
	require.Empty(t, appConfig.MDM.EndUserAuthentication.SSOProviderSettings.IssuerURI)
	require.Empty(t, appConfig.MDM.EndUserAuthentication.SSOProviderSettings.Metadata)
	require.Empty(t, appConfig.MDM.EndUserAuthentication.SSOProviderSettings.MetadataURL)
	require.Empty(t, appConfig.MDM.EndUserAuthentication.SSOProviderSettings.IDPName)
}

func TestGitOpsTeamConditionalAccess(t *testing.T) {
	teamName := "TestTeamConditionalAccess"

	ds, _, savedTeams := testing_utils.SetupFullGitOpsPremiumServer(t)

	ds.ConditionalAccessMicrosoftGetFunc = func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
		return &fleet.ConditionalAccessMicrosoftIntegration{}, nil
	}

	// Create integration with conditional access enabled.
	_, err := ds.NewTeam(context.Background(), &fleet.Team{Name: teamName, Config: fleet.TeamConfig{
		Integrations: fleet.TeamIntegrations{
			ConditionalAccessEnabled: optjson.SetBool(true),
		},
	}})
	require.NoError(t, err)
	require.NotNil(t, *savedTeams[teamName])

	// Do a GitOps run with conditional access not set.
	t.Setenv("TEST_TEAM_NAME", teamName)
	_, err = RunAppNoChecks([]string{"gitops", "-f", "testdata/gitops/team_config_webhook.yml"})
	require.NoError(t, err)

	team, err := ds.TeamByName(context.Background(), teamName)
	require.NoError(t, err)
	require.NotNil(t, team)
	require.True(t, team.Config.Integrations.ConditionalAccessEnabled.Set)
	require.False(t, team.Config.Integrations.ConditionalAccessEnabled.Value)
}

func TestGitOpsNoTeamConditionalAccess(t *testing.T) {
	globalFileBasic := createGlobalFileBasic(t, fleetServerURL, orgName)
	ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)

	ds.ConditionalAccessMicrosoftGetFunc = func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
		return &fleet.ConditionalAccessMicrosoftIntegration{}, nil
	}

	appConfig := fleet.AppConfig{
		Integrations: fleet.Integrations{
			ConditionalAccessEnabled: optjson.SetBool(true),
		},
	}

	ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
		return &appConfig, nil
	}

	ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
		appConfig = *config
		return nil
	}

	// Do a GitOps run with conditional access not set.
	_, err := RunAppNoChecks([]string{"gitops", "-f", globalFileBasic.Name()})
	require.NoError(t, err)
	require.True(t, appConfig.Integrations.ConditionalAccessEnabled.Set)
	require.False(t, appConfig.Integrations.ConditionalAccessEnabled.Value)
}

func TestGitOpsEULASetting(t *testing.T) {
	createGlobalGitOpsConfig := func(mdm string) string {
		return fmt.Sprintf(`
controls:
queries:
policies:
agent_options:
software:
org_settings:
  server_settings:
    server_url: "https://foo.example.com"
  org_info:
    org_name: GitOps Test
  secrets:
    - secret: "global"
  mdm:
    %s
`, mdm)
	}

	// Create a temporary PDF file
	pdfContent := []byte("%PDF-1\npdf-test")
	tmpPDF, err := os.CreateTemp(t.TempDir(), "*.pdf")
	require.NoError(t, err)
	// Write a minimal valid PDF header so the file is recognized as a PDF.
	_, err = tmpPDF.Write(pdfContent)
	require.NoError(t, err)
	pdfPath, err := filepath.Abs(tmpPDF.Name())
	require.NoError(t, err)

	// Create an invalid temp PDF file
	tmpInvalidPDF, err := os.CreateTemp(t.TempDir(), "*.txt")
	require.NoError(t, err)
	_, err = tmpInvalidPDF.Write([]byte("not-a-pdf"))
	require.NoError(t, err)
	invalidPDFPath, err := filepath.Abs(tmpInvalidPDF.Name())
	require.NoError(t, err)

	cases := []struct {
		name             string
		cfg              string
		mockSetup        func(t *testing.T, ds *mock.Store, gitopsDir string)
		dryRunAssertion  func(t *testing.T, ds *mock.Store, out string, err error)
		realRunAssertion func(t *testing.T, ds *mock.Store, out string, err error)
	}{
		{
			name: "valid pdf file (no existing EULA uploaded)",
			cfg:  createGlobalGitOpsConfig(fmt.Sprintf(`end_user_license_agreement: "%s"`, pdfPath)),
			mockSetup: func(t *testing.T, ds *mock.Store, dir string) {
				ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
					return nil, &notFoundError{} // No existing EULA
				}
			},
			dryRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
				assert.NoError(t, err)
				assert.Contains(t, out, "[+] would've applied EULA")
				assert.False(t, ds.MDMInsertEULAFuncInvoked)
			},
			realRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
				assert.NoError(t, err)
				assert.Contains(t, out, "[+] applied EULA")
				assert.True(t, ds.MDMInsertEULAFuncInvoked)
			},
		},
		{
			name: "relative path to working dir to pdf file (no existing EULA uploaded)",
			cfg:  createGlobalGitOpsConfig(`end_user_license_agreement: "./testdata/gitops/tiny_eula.pdf"`),
			mockSetup: func(t *testing.T, ds *mock.Store, dir string) {
				ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
					return nil, &notFoundError{} // No existing EULA
				}
			},
			dryRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
				assert.Error(t, err)
				assert.ErrorContains(t, err, "error uploading EULA: reading eula file:")
				assert.ErrorContains(t, err, "no such file or directory")
				assert.False(t, ds.MDMInsertEULAFuncInvoked)
			},
			realRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
				assert.Error(t, err)
				assert.ErrorContains(t, err, "error uploading EULA: reading eula file:")
				assert.ErrorContains(t, err, "no such file or directory")
				assert.False(t, ds.MDMInsertEULAFuncInvoked)
			},
		},
		{
			name: "relative path to yaml file to pdf file (no existing EULA uploaded)",
			cfg:  createGlobalGitOpsConfig(`end_user_license_agreement: "./lib/eula.pdf"`),
			mockSetup: func(t *testing.T, ds *mock.Store, dir string) {
				err := os.Mkdir(filepath.Join(dir, "lib"), 0o755)
				require.NoError(t, err)
				tmpPDF, err := os.Create(filepath.Join(dir, "lib", "eula.pdf"))
				require.NoError(t, err)
				_, err = tmpPDF.Write(pdfContent)
				require.NoError(t, err)
				err = tmpPDF.Close()
				require.NoError(t, err)

				ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
					return nil, &notFoundError{} // No existing EULA
				}
			},
			dryRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
				assert.NoError(t, err)
				assert.Contains(t, out, "[+] would've applied EULA")
				assert.False(t, ds.MDMInsertEULAFuncInvoked)
			},
			realRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
				assert.NoError(t, err)
				assert.Contains(t, out, "[+] applied EULA")
				assert.True(t, ds.MDMInsertEULAFuncInvoked)
			},
		},
		{
			name: "valid new pdf file (different EULA already uploaded)",
			cfg:  createGlobalGitOpsConfig(fmt.Sprintf(`end_user_license_agreement: "%s"`, pdfPath)),
			mockSetup: func(t *testing.T, ds *mock.Store, dir string) {
				ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
					return &fleet.MDMEULA{
						Name:  pdfPath,
						Token: "test-token",
					}, nil
				}
			},
			dryRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
				assert.NoError(t, err)
				assert.Contains(t, out, "[+] would've applied EULA")
				assert.False(t, ds.MDMDeleteEULAFuncInvoked)
				assert.False(t, ds.MDMInsertEULAFuncInvoked)
			},
			realRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
				assert.NoError(t, err)
				assert.Contains(t, out, "[+] applied EULA")
				assert.True(t, ds.MDMDeleteEULAFuncInvoked) // deleted old EULA
				assert.True(t, ds.MDMInsertEULAFuncInvoked) // new EULA was updated
			},
		},
		{
			name: "no EULA specified (no existing EULA uploaded)",
			cfg:  createGlobalGitOpsConfig(""),
			mockSetup: func(t *testing.T, ds *mock.Store, dir string) {
				ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
					return nil, &notFoundError{} // No existing EULA
				}
			},
			dryRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
				assert.NoError(t, err)
				assert.Contains(t, out, "[+] would've applied EULA")
				assert.False(t, ds.MDMDeleteEULAFuncInvoked)
				assert.False(t, ds.MDMInsertEULAFuncInvoked)
			},
			realRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
				assert.NoError(t, err)
				assert.Contains(t, out, "[+] applied EULA")
				assert.False(t, ds.MDMDeleteEULAFuncInvoked) // no EULA to delete
				assert.False(t, ds.MDMInsertEULAFuncInvoked) // no EULA to upload
			},
		},
		{
			name: "deleting existing EULA",
			cfg:  createGlobalGitOpsConfig(""),
			mockSetup: func(t *testing.T, ds *mock.Store, dir string) {
				ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
					return &fleet.MDMEULA{
						Name:  pdfPath,
						Token: "test-token",
					}, nil
				}
			},
			dryRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
				assert.NoError(t, err)
				assert.Contains(t, out, "[+] would've applied EULA")
				assert.False(t, ds.MDMDeleteEULAFuncInvoked)
			},
			realRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
				assert.NoError(t, err)
				assert.Contains(t, out, "[+] applied EULA")
				assert.True(t, ds.MDMDeleteEULAFuncInvoked) // deleted EULA
			},
		},
		{
			name: "not a PDF file",
			cfg:  createGlobalGitOpsConfig(fmt.Sprintf(`end_user_license_agreement: "%s"`, invalidPDFPath)),
			mockSetup: func(t *testing.T, ds *mock.Store, dir string) {
				ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
					return nil, &notFoundError{} // No existing EULA
				}
			},
			dryRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
				assert.ErrorContains(t, err, "invalid file type")
			},
			realRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
				assert.ErrorContains(t, err, "invalid file type")
			},
		},
		{
			name: "uploading the same EULA again",
			cfg:  createGlobalGitOpsConfig(""),
			mockSetup: func(t *testing.T, ds *mock.Store, dir string) {
				ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
					hash := sha256.Sum256(pdfContent) // Simulate same EULA
					return &fleet.MDMEULA{
						Name:   pdfPath,
						Token:  "test-token",
						Sha256: hash[:], // Simulate same EULA
					}, nil
				}
			},
			dryRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
				assert.NoError(t, err)
				assert.Contains(t, out, "[+] would've applied EULA")
				assert.False(t, ds.MDMInsertEULAFuncInvoked)
			},
			realRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
				assert.NoError(t, err)
				assert.Contains(t, out, "[+] applied EULA")
				assert.False(t, ds.MDMInsertEULAFuncInvoked) // No new EULA uploaded
			},
		},
	}

	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)

			// these mocks are used for all tests
			ds.MDMInsertEULAFunc = func(ctx context.Context, eula *fleet.MDMEULA) error {
				return nil
			}
			ds.MDMDeleteEULAFunc = func(ctx context.Context, token string) error {
				return nil
			}

			tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
			require.NoError(t, err)
			_, err = tmpFile.WriteString(tt.cfg)
			require.NoError(t, err)

			// these mocks are defined in the individual test cases
			tt.mockSetup(t, ds, filepath.Dir(tmpFile.Name()))

			// Dry run
			out, err := RunAppNoChecks([]string{"gitops", "-f", tmpFile.Name(), "--dry-run"})
			tt.dryRunAssertion(t, ds, out.String(), err)
			if t.Failed() {
				t.FailNow()
			}

			// Real run
			out, err = RunAppNoChecks([]string{"gitops", "-f", tmpFile.Name()})
			tt.realRunAssertion(t, ds, out.String(), err)
		})
	}
}

// setupAndroidCertificatesTestMocks sets up common mocks for Android certificate GitOps tests
func setupAndroidCertificatesTestMocks(t *testing.T, ds *mock.Store) []*fleet.CertificateAuthority {
	// Set up certificate authority mocks
	certAuthorities := []*fleet.CertificateAuthority{
		{
			ID:        1,
			Name:      ptr.String("Test CA 1"),
			Type:      string(fleet.CATypeCustomSCEPProxy),
			URL:       ptr.String("https://ca1.example.com"),
			Challenge: ptr.String("challenge1"),
		},
		{
			ID:        2,
			Name:      ptr.String("Test CA 2"),
			Type:      string(fleet.CATypeCustomSCEPProxy),
			URL:       ptr.String("https://ca2.example.com"),
			Challenge: ptr.String("challenge2"),
		},
		{
			ID:                            3,
			Name:                          ptr.String("Test CA 3"),
			Type:                          string(fleet.CATypeDigiCert),
			URL:                           ptr.String("https://ca3.example.com"),
			Challenge:                     ptr.String("challenge3"),
			CertificateCommonName:         ptr.String("foo"),
			CertificateSeatID:             ptr.String("foo"),
			CertificateUserPrincipalNames: &[]string{"foo"},
			APIToken:                      ptr.String("foo"),
			ProfileID:                     ptr.String("foo"),
		},
	}

	ds.BatchSetMDMProfilesFunc = func(
		ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile,
		winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration,
		androidProfiles []*fleet.MDMAndroidConfigProfile, vars []fleet.MDMProfileIdentifierFleetVariables,
	) (updates fleet.MDMProfilesUpdates, err error) {
		return fleet.MDMProfilesUpdates{}, nil
	}

	ds.BulkSetPendingMDMHostProfilesFunc = func(
		ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
	) (updates fleet.MDMProfilesUpdates, err error) {
		return fleet.MDMProfilesUpdates{}, nil
	}

	ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
		summaries := make([]*fleet.CertificateAuthoritySummary, 0, len(certAuthorities))
		for _, ca := range certAuthorities {
			summaries = append(summaries, &fleet.CertificateAuthoritySummary{
				ID:   ca.ID,
				Name: *ca.Name,
				Type: ca.Type,
			})
		}
		return summaries, nil
	}

	ds.GetGroupedCertificateAuthoritiesFunc = func(ctx context.Context, includeSecrets bool) (*fleet.GroupedCertificateAuthorities, error) {
		cas := make([]*fleet.CertificateAuthority, len(certAuthorities))
		copy(cas, certAuthorities)
		grouped, err := fleet.GroupCertificateAuthoritiesByType(cas)
		return grouped, err
	}

	// Override LabelIDsByNameFunc to handle empty labels
	ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
		if len(labels) == 0 {
			return map[string]uint{}, nil
		}
		return map[string]uint{fleet.BuiltinLabelMacOS14Plus: 1}, nil
	}

	return certAuthorities
}

// TestGitOpsAndroidCertificatesAdd tests adding Android certificates via GitOps
func TestGitOpsAndroidCertificatesAdd(t *testing.T) {
	ds, _, savedTeams := testing_utils.SetupFullGitOpsPremiumServer(t)
	setupAndroidCertificatesTestMocks(t, ds)

	// Track certificate templates that are created
	var createdCertificates []fleet.CertificateTemplate

	ds.BatchUpsertCertificateTemplatesFunc = func(ctx context.Context, certificates []*fleet.CertificateTemplate) error {
		createdCertificates = nil
		for _, cert := range certificates {
			createdCertificates = append(createdCertificates, *cert)
		}
		return nil
	}

	ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, options fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
		return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
	}

	ds.BatchDeleteCertificateTemplatesFunc = func(ctx context.Context, ids []uint) error {
		return nil
	}

	// Create team config
	teamConfig := `
name: %s
team_settings:
  secrets:
    - secret: TestSecret
agent_options:
  config:
    options:
      pack_delimiter: /
  overrides: {}
controls:
  android_settings:
    certificates:
      - name: "Certificate 1"
        certificate_authority_name: "Test CA %d"
        subject_name: "CN=Device Certificate 1"
      - name: "Certificate 2"
        certificate_authority_name: "Test CA %d"
        subject_name: "CN=Device Certificate 2"
policies: []
queries: []
software: null
`

	tmpDir := t.TempDir()
	teamFile, err := os.CreateTemp(tmpDir, "team-*.yml")
	require.NoError(t, err)
	_, err = teamFile.WriteString(fmt.Sprintf(teamConfig, teamName, 1, 2))
	require.NoError(t, err)

	// Run GitOps
	_, err = RunAppNoChecks([]string{"gitops", "-f", teamFile.Name()})
	require.NoError(t, err)

	// Verify certificates were created
	require.Len(t, createdCertificates, 2)
	assert.Equal(t, "Certificate 1", createdCertificates[0].Name)
	assert.Equal(t, uint(1), createdCertificates[0].CertificateAuthorityID)
	assert.Equal(t, "CN=Device Certificate 1", createdCertificates[0].SubjectName)
	assert.Equal(t, "Certificate 2", createdCertificates[1].Name)
	assert.Equal(t, uint(2), createdCertificates[1].CertificateAuthorityID)
	assert.Equal(t, "CN=Device Certificate 2", createdCertificates[1].SubjectName)

	// Verify team was created
	require.Contains(t, savedTeams, teamName)

	// Try the same with a cert that uses a DigiCert CA
	_, err = teamFile.WriteString(fmt.Sprintf(teamConfig, teamName, 2, 3))
	require.NoError(t, err)
	_, err = RunAppNoChecks([]string{"gitops", "-f", teamFile.Name()})
	require.Error(t, err)
	require.Contains(t, err.Error(), "Currently, only the custom_scep_proxy certificate authority is supported")

	// Try the same with a non-existent CA
	_, err = teamFile.WriteString(fmt.Sprintf(teamConfig, teamName, 2, 4))
	require.NoError(t, err)
	_, err = RunAppNoChecks([]string{"gitops", "-f", teamFile.Name()})
	require.Error(t, err)
	require.Contains(t, err.Error(), "not found")
}

// TestGitOpsAndroidCertificatesChange tests changing existing Android certificates via GitOps
func TestGitOpsAndroidCertificatesChange(t *testing.T) {
	ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
	setupAndroidCertificatesTestMocks(t, ds)

	// Track certificate templates actions
	var updatedCertificates []fleet.CertificateTemplate
	var deletedCertificateIDs []uint

	ds.BatchUpsertCertificateTemplatesFunc = func(ctx context.Context, certificates []*fleet.CertificateTemplate) error {
		updatedCertificates = nil
		for _, cert := range certificates {
			updatedCertificates = append(updatedCertificates, *cert)
		}
		return nil
	}

	// Simulate existing certificates
	ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, options fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
		existing := []*fleet.CertificateTemplateResponseSummary{
			{
				ID:                     1,
				Name:                   "Certificate 1",
				CertificateAuthorityId: 1,
			},
			{
				ID:                     2,
				Name:                   "Certificate 2",
				CertificateAuthorityId: 2,
			},
		}
		return existing, &fleet.PaginationMetadata{}, nil
	}

	// Simulate full certificate details for existing certificates
	ds.GetCertificateTemplateByIdFunc = func(ctx context.Context, id uint) (*fleet.CertificateTemplateResponseFull, error) {
		switch id {
		case 1:
			return &fleet.CertificateTemplateResponseFull{
				CertificateTemplateResponseSummary: fleet.CertificateTemplateResponseSummary{
					ID:                     1,
					Name:                   "Certificate 1",
					CertificateAuthorityId: 1,
				},
				SubjectName: "CN=Original Subject 1",
			}, nil
		case 2:
			return &fleet.CertificateTemplateResponseFull{
				CertificateTemplateResponseSummary: fleet.CertificateTemplateResponseSummary{
					ID:                     2,
					Name:                   "Certificate 2",
					CertificateAuthorityId: 2,
				},
				SubjectName: "CN=Original Subject 2",
			}, nil
		default:
			return nil, errors.New("certificate not found")
		}
	}

	ds.BatchDeleteCertificateTemplatesFunc = func(ctx context.Context, ids []uint) error {
		deletedCertificateIDs = append(deletedCertificateIDs, ids...)
		return nil
	}

	// Create team config with modified subjectNames
	teamConfig := `
name: %s
team_settings:
  secrets:
    - secret: TestSecret
  mdm:
    macos_updates:
      minimum_version: null
      deadline: null
    macos_settings:
      custom_settings: null
    macos_setup:
      bootstrap_package: null
      enable_end_user_authentication: false
      macos_setup_assistant: null
    windows_updates:
      deadline_days: null
      grace_period_days: null
    windows_settings:
      custom_settings: null
agent_options:
  config:
    options:
      pack_delimiter: /
  overrides: {}
controls:
  android_settings:
    certificates:
      - name: "Certificate 1"
        certificate_authority_name: "Test CA 1"
        subject_name: "CN=Updated Subject 1"
      - name: "Certificate 2"
        certificate_authority_name: "Test CA 2"
        subject_name: "CN=Updated Subject 2"
policies: []
queries: []
software: null
`

	tmpDir := t.TempDir()
	teamFile, err := os.CreateTemp(tmpDir, "team-*.yml")
	require.NoError(t, err)
	_, err = teamFile.WriteString(fmt.Sprintf(teamConfig, teamName))
	require.NoError(t, err)

	// Run GitOps
	_, err = RunAppNoChecks([]string{"gitops", "-f", teamFile.Name()})
	require.NoError(t, err)

	// Verify both certificates were deleted because their SubjectName changed
	require.Len(t, deletedCertificateIDs, 2, "Both certificates should be deleted due to SubjectName changes")
	assert.Contains(t, deletedCertificateIDs, uint(1), "Certificate 1 should be deleted")
	assert.Contains(t, deletedCertificateIDs, uint(2), "Certificate 2 should be deleted")

	// Verify both certificates were recreated with new subject names
	require.Len(t, updatedCertificates, 2, "Both certificates should be recreated")
	assert.Equal(t, "Certificate 1", updatedCertificates[0].Name)
	assert.Equal(t, "CN=Updated Subject 1", updatedCertificates[0].SubjectName)
	assert.Equal(t, "Certificate 2", updatedCertificates[1].Name)
	assert.Equal(t, "CN=Updated Subject 2", updatedCertificates[1].SubjectName)

	// Create team config with modified certificate authorities (swapped CAs)
	updatedCertificates = make([]fleet.CertificateTemplate, 0)
	deletedCertificateIDs = make([]uint, 0)
	teamConfig = `
name: %s
team_settings:
  secrets:
    - secret: TestSecret
  mdm:
    macos_updates:
      minimum_version: null
      deadline: null
    macos_settings:
      custom_settings: null
    macos_setup:
      bootstrap_package: null
      enable_end_user_authentication: false
      macos_setup_assistant: null
    windows_updates:
      deadline_days: null
      grace_period_days: null
    windows_settings:
      custom_settings: null
agent_options:
  config:
    options:
      pack_delimiter: /
  overrides: {}
controls:
  android_settings:
    certificates:
      - name: "Certificate 1"
        certificate_authority_name: "Test CA 2"
        subject_name: "CN=Original Subject 1"
      - name: "Certificate 2"
        certificate_authority_name: "Test CA 1"
        subject_name: "CN=Original Subject 2"
policies: []
queries: []
software: null
`

	teamFile, err = os.CreateTemp(tmpDir, "team-*.yml")
	require.NoError(t, err)
	_, err = teamFile.WriteString(fmt.Sprintf(teamConfig, teamName))
	require.NoError(t, err)

	// Run GitOps
	_, err = RunAppNoChecks([]string{"gitops", "-f", teamFile.Name()})
	require.NoError(t, err)

	// Verify both certificates were deleted because their SubjectName changed
	require.Len(t, deletedCertificateIDs, 2, "Both certificates should be deleted due to SubjectName changes")
	assert.Contains(t, deletedCertificateIDs, uint(1), "Certificate 1 should be deleted")
	assert.Contains(t, deletedCertificateIDs, uint(2), "Certificate 2 should be deleted")

	// Verify both certificates were recreated with new subject names
	require.Len(t, updatedCertificates, 2, "Both certificates should be recreated")
	assert.Equal(t, "Certificate 1", updatedCertificates[0].Name)
	assert.Equal(t, uint(2), updatedCertificates[0].CertificateAuthorityID)
	assert.Equal(t, "Certificate 2", updatedCertificates[1].Name)
	assert.Equal(t, uint(1), updatedCertificates[1].CertificateAuthorityID)

	// Create team config with no changes, make sure we don't delete
	updatedCertificates = make([]fleet.CertificateTemplate, 0)
	deletedCertificateIDs = make([]uint, 0)
	teamConfig = `
name: %s
team_settings:
  secrets:
    - secret: TestSecret
  mdm:
    macos_updates:
      minimum_version: null
      deadline: null
    macos_settings:
      custom_settings: null
    macos_setup:
      bootstrap_package: null
      enable_end_user_authentication: false
      macos_setup_assistant: null
    windows_updates:
      deadline_days: null
      grace_period_days: null
    windows_settings:
      custom_settings: null
agent_options:
  config:
    options:
      pack_delimiter: /
  overrides: {}
controls:
  android_settings:
    certificates:
      - name: "Certificate 1"
        certificate_authority_name: "Test CA 1"
        subject_name: "CN=Original Subject 1"
      - name: "Certificate 2"
        certificate_authority_name: "Test CA 2"
        subject_name: "CN=Original Subject 2"
policies: []
queries: []
software: null
`

	teamFile, err = os.CreateTemp(tmpDir, "team-*.yml")
	require.NoError(t, err)
	_, err = teamFile.WriteString(fmt.Sprintf(teamConfig, teamName))
	require.NoError(t, err)

	// Run GitOps
	_, err = RunAppNoChecks([]string{"gitops", "-f", teamFile.Name()})
	require.NoError(t, err)

	require.Len(t, deletedCertificateIDs, 0, "No certificates should be deleted when there are no changes")
	require.Len(t, updatedCertificates, 2, "Both certificates should be present without changes")
}

// TestGitOpsAndroidCertificatesDeleteOne tests deleting one certificate while leaving others via GitOps
func TestGitOpsAndroidCertificatesDeleteOne(t *testing.T) {
	ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
	setupAndroidCertificatesTestMocks(t, ds)

	// Track what was deleted
	var deletedCertificateIDs []uint
	var remainingCertificates []fleet.CertificateTemplate

	ds.BatchDeleteCertificateTemplatesFunc = func(ctx context.Context, ids []uint) error {
		deletedCertificateIDs = ids
		return nil
	}

	ds.BatchUpsertCertificateTemplatesFunc = func(ctx context.Context, certificates []*fleet.CertificateTemplate) error {
		remainingCertificates = nil
		for _, cert := range certificates {
			remainingCertificates = append(remainingCertificates, *cert)
		}
		return nil
	}

	// Simulate existing certificates
	ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, options fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
		existing := []*fleet.CertificateTemplateResponseSummary{
			{
				ID:                     1,
				Name:                   "Certificate 1",
				CertificateAuthorityId: 1,
			},
			{
				ID:                     2,
				Name:                   "Certificate 2",
				CertificateAuthorityId: 2,
			},
		}
		return existing, &fleet.PaginationMetadata{}, nil
	}

	// Mock GetCertificateTemplateByIdFunc to return full certificate details
	ds.GetCertificateTemplateByIdFunc = func(ctx context.Context, id uint) (*fleet.CertificateTemplateResponseFull, error) {
		switch id {
		case 1:
			return &fleet.CertificateTemplateResponseFull{
				CertificateTemplateResponseSummary: fleet.CertificateTemplateResponseSummary{
					ID:                     1,
					Name:                   "Certificate 1",
					CertificateAuthorityId: 1,
				},
				SubjectName: "CN=Device Certificate 1",
			}, nil
		case 2:
			return &fleet.CertificateTemplateResponseFull{
				CertificateTemplateResponseSummary: fleet.CertificateTemplateResponseSummary{
					ID:                     2,
					Name:                   "Certificate 2",
					CertificateAuthorityId: 2,
				},
				SubjectName: "CN=Device Certificate 2",
			}, nil
		default:
			return nil, errors.New("certificate not found")
		}
	}

	// Create team config with only one certificate (Certificate 1 removed)
	teamConfig := `
name: %s
team_settings:
  secrets:
    - secret: TestSecret
  mdm:
    macos_updates:
      minimum_version: null
      deadline: null
    macos_settings:
      custom_settings: null
    macos_setup:
      bootstrap_package: null
      enable_end_user_authentication: false
      macos_setup_assistant: null
    windows_updates:
      deadline_days: null
      grace_period_days: null
    windows_settings:
      custom_settings: null
agent_options:
  config:
    options:
      pack_delimiter: /
  overrides: {}
controls:
  android_settings:
    certificates:
      - name: "Certificate 2"
        certificate_authority_name: "Test CA 2"
        subject_name: "CN=Device Certificate 2"
policies: []
queries: []
software: null
`

	tmpDir := t.TempDir()
	teamFile, err := os.CreateTemp(tmpDir, "team-*.yml")
	require.NoError(t, err)
	_, err = teamFile.WriteString(fmt.Sprintf(teamConfig, teamName))
	require.NoError(t, err)

	// Run GitOps
	out, err := RunAppNoChecks([]string{"gitops", "-f", teamFile.Name()})
	require.NoError(t, err)

	// Verify output mentions deletion
	output := out.String()
	assert.True(t, strings.Contains(output, "deleting") || strings.Contains(output, "deleted"),
		"Expected deletion message in output: %s", output)

	// Verify Certificate 1 was deleted (ID 1)
	require.Len(t, deletedCertificateIDs, 1)
	assert.Contains(t, deletedCertificateIDs, uint(1))

	// Verify Certificate 2 remains
	require.Len(t, remainingCertificates, 1)
	assert.Equal(t, "Certificate 2", remainingCertificates[0].Name)
	assert.Equal(t, "CN=Device Certificate 2", remainingCertificates[0].SubjectName)
}

// TestGitOpsAndroidCertificatesDeleteAll tests deleting all certificates via GitOps
func TestGitOpsAndroidCertificatesDeleteAll(t *testing.T) {
	ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
	setupAndroidCertificatesTestMocks(t, ds)

	// Track what was deleted
	var deletedCertificateIDs []uint

	ds.BatchDeleteCertificateTemplatesFunc = func(ctx context.Context, ids []uint) error {
		deletedCertificateIDs = ids
		return nil
	}

	// Simulate existing certificates
	ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, options fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
		existing := []*fleet.CertificateTemplateResponseSummary{
			{
				ID:                     1,
				Name:                   "Certificate 1",
				CertificateAuthorityId: 1,
			},
			{
				ID:                     2,
				Name:                   "Certificate 2",
				CertificateAuthorityId: 2,
			},
		}
		return existing, &fleet.PaginationMetadata{}, nil
	}

	// Create team config with no certificates
	teamConfig := `
name: %s
team_settings:
  secrets:
    - secret: TestSecret
  mdm:
    macos_updates:
      minimum_version: null
      deadline: null
    macos_settings:
      custom_settings: null
    macos_setup:
      bootstrap_package: null
      enable_end_user_authentication: false
      macos_setup_assistant: null
    windows_updates:
      deadline_days: null
      grace_period_days: null
    windows_settings:
      custom_settings: null
agent_options:
  config:
    options:
      pack_delimiter: /
  overrides: {}
controls:
  android_settings:
    certificates: []
policies: []
queries: []
software: null
`

	tmpDir := t.TempDir()
	teamFile, err := os.CreateTemp(tmpDir, "team-*.yml")
	require.NoError(t, err)
	_, err = teamFile.WriteString(fmt.Sprintf(teamConfig, teamName))
	require.NoError(t, err)

	// Run GitOps
	out, err := RunAppNoChecks([]string{"gitops", "-f", teamFile.Name()})
	require.NoError(t, err)

	// Verify output mentions deletion
	output := out.String()
	assert.True(t, strings.Contains(output, "deleting") || strings.Contains(output, "deleted"),
		"Expected deletion message in output: %s", output)

	// Verify both certificates were deleted
	require.Len(t, deletedCertificateIDs, 2)
	assert.Contains(t, deletedCertificateIDs, uint(1))
	assert.Contains(t, deletedCertificateIDs, uint(2))
}
